Cves
Overview
Impact:
Description: Processing maliciously crafted web content may lead to memory corruption
Researchers: Dung Do (@_piers2) of Calif.io
Key insight. A no-copy WebGPU upload aliased caller storage the GPU read asynchronously without keeping it alive, and an array-length binding index was bumped by an unchecked uint32 add that could wrap onto a user binding — retaining the storage until GPU completion and using checkedSum close both.
Patch Details
diff --git a/Source/WebGPU/WebGPU/Queue.mm b/Source/WebGPU/WebGPU/Queue.mm
index 0bf08a83aa85..90851766fcfd 100644
--- a/Source/WebGPU/WebGPU/Queue.mm
+++ b/Source/WebGPU/WebGPU/Queue.mm
@@ -48,7 +48,7 @@
namespace WebGPU {
-constexpr static auto largeBufferSize = 32 * 1024 * 1024;
+constexpr static auto largeBufferSize = WGPU_LARGE_BUFFER_SIZE;
constexpr bool skipMemoryAttribution = true;
WTF_MAKE_TZONE_ALLOCATED_IMPL(Queue);
@@ -1225,8 +1225,16 @@ void Queue::writeTexture(const WGPUImageCopyTexture& destination, std::span<uint
return;
}
- if (noCopy)
+ if (noCopy) {
+ if (!newData.isEmpty()) {
+ // The MTLBuffer above was created with newBufferWithBytesNoCopy and aliases newData's storage; keep that storage alive until the GPU has consumed it.
+ __block Vector<uint8_t> retainedNewData = WTF::move(newData);
+ [m_commandBuffer addCompletedHandler:^(id<MTLCommandBuffer>) {
+ retainedNewData = { };
+ }];
+ }
finalizeBlitCommandEncoder();
+ }
}
void Queue::setLabel(String&& label)
diff --git a/Source/WebGPU/WebGPU/Queue.swift b/Source/WebGPU/WebGPU/Queue.swift
index ef2827dc6aff..d9838d068a28 100644
--- a/Source/WebGPU/WebGPU/Queue.swift
+++ b/Source/WebGPU/WebGPU/Queue.swift
@@ -25,8 +25,9 @@
public import Metal
internal import WebGPU_Internal.Queue
+import WebGPU_Private.WebGPUExt
-private let largeBufferSize = 32 * 1024 * 1024
+private let largeBufferSize = Int(WGPU_LARGE_BUFFER_SIZE)
// FIXME: Eventually all these "thunks" should be removed.
// swift-format-ignore: AlwaysUseLowerCamelCase
diff --git a/Source/WebGPU/WebGPU/RenderPipeline.mm b/Source/WebGPU/WebGPU/RenderPipeline.mm
index 8b446ce17590..9d40a2908f85 100644
--- a/Source/WebGPU/WebGPU/RenderPipeline.mm
+++ b/Source/WebGPU/WebGPU/RenderPipeline.mm
@@ -982,6 +982,13 @@ NSString* Device::addPipelineLayouts(Vector<Vector<WGPUBindGroupLayoutEntry>>& p
for (auto& bindGroupLayout : pipelineLayout.bindGroupLayouts) {
auto& entries = pipelineEntries[bindGroupLayout.group];
HashMap<String, uint64_t> entryMap;
+ // Wrapping the bump would let the array-length entry alias a user binding and defeat the bounds check.
+ auto bumpForArrayLength = [&](uint32_t webBinding) -> std::optional<uint32_t> {
+ auto checked = checkedSum<uint32_t>(webBinding, limits().maxBindingsPerBindGroup);
+ if (checked.hasOverflowed())
+ return std::nullopt;
+ return checked.value();
+ };
for (auto& entry : bindGroupLayout.entries) {
auto visibility = convertVisibility(entry.visibility);
auto stage = visibility / 2;
@@ -991,7 +998,10 @@ NSString* Device::addPipelineLayouts(Vector<Vector<WGPUBindGroupLayoutEntry>>& p
uint32_t webBinding = entry.webBinding;
if (auto& entryName = entry.name; entryName.length()) {
if (entryName.endsWith("_ArrayLength"_s)) {
- webBinding += limits().maxBindingsPerBindGroup;
+ auto bumped = bumpForArrayLength(webBinding);
+ if (!bumped)
+ return @"Binding index overflow in auto-generated layouts";
+ webBinding = *bumped;
isArrayLength = true;
}
}
@@ -1010,7 +1020,10 @@ NSString* Device::addPipelineLayouts(Vector<Vector<WGPUBindGroupLayoutEntry>>& p
WGPUBufferBindingType bufferTypeOverride = WGPUBufferBindingType_Undefined;
if (auto& entryName = entry.name; entryName.length()) {
if (isArrayLength) {
- webBinding += limits().maxBindingsPerBindGroup;
+ auto bumped = bumpForArrayLength(webBinding);
+ if (!bumped)
+ return @"Binding index overflow in auto-generated layouts";
+ webBinding = *bumped;
bufferTypeOverride = static_cast<WGPUBufferBindingType>(WGPUBufferBindingType_ArrayLength);
auto shortName = entryName.substring(2, entryName.length() - (sizeof("_ArrayLength") + 1));
if (auto it = entryMap.find(shortName); it != entryMap.end())
diff --git a/Source/WebGPU/WebGPU/WebGPUExt.h b/Source/WebGPU/WebGPU/WebGPUExt.h
index 5fec3ec7cceb..fcdd849578ed 100644
--- a/Source/WebGPU/WebGPU/WebGPUExt.h
+++ b/Source/WebGPU/WebGPU/WebGPUExt.h
@@ -41,6 +41,13 @@
#define WGPU_FUZZER_ASSERT_NOT_REACHED(...) WTFLogAlways(__VA_ARGS__)
#endif
+// Threshold above which the Metal backend uses newBufferWithBytesNoCopy in writeBuffer / writeTexture
+// and aliases the caller's storage rather than copying. Callers passing transfers >= this size MUST
+// keep the source bytes alive until the GPU has consumed them (e.g. via addCompletedHandler).
+// Value is 32 * 1024 * 1024; written as a single integer literal so Swift's clang macro importer
+// picks it up as `WGPU_LARGE_BUFFER_SIZE` rather than skipping it.
+#define WGPU_LARGE_BUFFER_SIZE 33554432
+
#include <optional>
#include <simd/simd.h>
#include <wtf/MachSendRight.h>
diff --git a/Source/WebKit/GPUProcess/graphics/WebGPU/RemoteQueue.cpp b/Source/WebKit/GPUProcess/graphics/WebGPU/RemoteQueue.cpp
index 89b29f347c47..5d54a354824c 100644
--- a/Source/WebKit/GPUProcess/graphics/WebGPU/RemoteQueue.cpp
+++ b/Source/WebKit/GPUProcess/graphics/WebGPU/RemoteQueue.cpp
@@ -35,12 +35,24 @@
#include <WebCore/SharedMemory.h>
#include <WebCore/WebGPUBuffer.h>
#include <WebCore/WebGPUQueue.h>
+#include <WebGPU/WebGPU.h>
+#include <WebGPU/WebGPUExt.h>
#include <wtf/TZoneMallocInlines.h>
namespace WebKit {
WTF_MAKE_TZONE_ALLOCATED_IMPL(RemoteQueue);
+// For transfers at or above WGPU_LARGE_BUFFER_SIZE the backend uses newBufferWithBytesNoCopy and aliases `data`'s mapping; keep it alive until the GPU has consumed the bytes. Smaller transfers are copied into a Metal buffer synchronously, so `data` can be released as soon as we return.
+static void keepAliveUntilSubmittedWorkDone(WebCore::WebGPU::Queue& backing, RefPtr<WebCore::SharedMemory>&& data)
+{
+ if (!data || data->size() < WGPU_LARGE_BUFFER_SIZE)
+ return;
+ backing.onSubmittedWorkDone([data = WTF::move(data)]() mutable {
+ data = nullptr;
+ });
+}
+
RemoteQueue::RemoteQueue(WebCore::WebGPU::Queue& queue, WebGPU::ObjectHeap& objectHeap, Ref<IPC::StreamServerConnection>&& streamConnection, RemoteGPU& gpu, WebGPUIdentifier identifier)
: m_backing(queue)
, m_objectHeap(objectHeap)
@@ -98,7 +110,9 @@ void RemoteQueue::writeBuffer(
return;
}
- protectedBacking()->writeBufferNoCopy(*convertedBuffer, bufferOffset, data->mutableSpan(), 0, std::nullopt);
+ Ref backing = m_backing;
+ backing->writeBufferNoCopy(*convertedBuffer, bufferOffset, data->mutableSpan(), 0, std::nullopt);
+ keepAliveUntilSubmittedWorkDone(backing, WTF::move(data));
completionHandler(true);
}
@@ -128,15 +142,17 @@ void RemoteQueue::writeTexture(
auto convertedDestination = objectHeap->convertFromBacking(destination);
ASSERT(convertedDestination);
auto convertedDataLayout = objectHeap->convertFromBacking(dataLayout);
- ASSERT(convertedDestination);
+ ASSERT(convertedDataLayout);
auto convertedSize = objectHeap->convertFromBacking(size);
ASSERT(convertedSize);
- if (!convertedDestination || !convertedDestination || !convertedSize || !data || data->size() <= WebGPU::maxCrossProcessResourceCopySize) {
+ if (!convertedDestination || !convertedDataLayout || !convertedSize || !data || data->size() <= WebGPU::maxCrossProcessResourceCopySize) {
completionHandler(false);
return;
}
- protectedBacking()->writeTexture(*convertedDestination, data ? data->mutableSpan() : std::span<uint8_t> { }, *convertedDataLayout, *convertedSize);
+ Ref backing = m_backing;
+ backing->writeTexture(*convertedDestination, data->mutableSpan(), *convertedDataLayout, *convertedSize);
+ keepAliveUntilSubmittedWorkDone(backing, WTF::move(data));
completionHandler(true);
}
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: wwwlk
Key insight. The nested credProps lookup reused the outer iterator and compared the submap’s find() result against the OUTER map’s end(), so a missing ‘rk’ key dereferenced an iterator from the wrong container.
Patch Details
diff --git a/Source/WebCore/Modules/webauthn/AuthenticationExtensionsClientOutputs.cpp b/Source/WebCore/Modules/webauthn/AuthenticationExtensionsClientOutputs.cpp
index 39563d3ba66d..897f6a34e6a9 100644
--- a/Source/WebCore/Modules/webauthn/AuthenticationExtensionsClientOutputs.cpp
+++ b/Source/WebCore/Modules/webauthn/AuthenticationExtensionsClientOutputs.cpp
@@ -49,9 +49,10 @@ std::optional<AuthenticationExtensionsClientOutputs> AuthenticationExtensionsCli
it = decodedMap.find(cbor::CBORValue("credProps"));
if (it != decodedMap.end() && it->second.isMap()) {
CredentialPropertiesOutput credProps;
- it = it->second.getMap().find(cbor::CBORValue("rk"));
- if (it != decodedMap.end() && it->second.isBool())
- credProps.rk = it->second.getBool();
+ const auto& credPropsMap = it->second.getMap();
+ auto credPropsIt = credPropsMap.find(cbor::CBORValue("rk"));
+ if (credPropsIt != credPropsMap.end() && credPropsIt->second.isBool())
+ credProps.rk = credPropsIt->second.getBool();
clientOutputs.credProps = credProps;
}
diff --git a/Tools/TestWebKitAPI/Tests/WebCore/CBORReaderTest.cpp b/Tools/TestWebKitAPI/Tests/WebCore/CBORReaderTest.cpp
index a8af4b1fa479..3b6c2d114d04 100644
--- a/Tools/TestWebKitAPI/Tests/WebCore/CBORReaderTest.cpp
+++ b/Tools/TestWebKitAPI/Tests/WebCore/CBORReaderTest.cpp
@@ -31,6 +31,7 @@
#if ENABLE(WEB_AUTHN)
+#include <WebCore/AuthenticationExtensionsClientOutputs.h>
#include <WebCore/CBORReader.h>
#include <limits>
#include <utility>
@@ -764,6 +765,20 @@ TEST(CBORReaderTest, TestUnsupportedSimplevalue)
}
}
+TEST(CBORReaderTest, AuthExtensionsFromCBOR_CredPropsWithoutRk)
+{
+ // CBOR encoding of {"credProps": {}} — credProps map present but no "rk" key.
+ // a1 -- map(1)
+ // 69 -- text(9)
+ // 63726564 50726f7073 -- "credProps"
+ // a0 -- map(0)
+ Vector<uint8_t> cborData { 0xa1, 0x69, 0x63, 0x72, 0x65, 0x64, 0x50, 0x72, 0x6f, 0x70, 0x73, 0xa0 };
+ auto result = WebCore::AuthenticationExtensionsClientOutputs::fromCBOR(cborData);
+ ASSERT_TRUE(result.has_value());
+ ASSERT_TRUE(result->credProps.has_value());
+ EXPECT_FALSE(result->credProps->rk);
+}
+
} // namespace TestWebKitAPI
#endif // ENABLE(WEB_AUTHN)
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: Hossein Lotfi (@hosselot) of TrendAI Zero Day Initiative
Key insight. Two optimizer passes disagreed about a Value’s lifetime: strength reduction deleted a Void value while PureCSE still held a raw pointer to it. The fix adds PureCSE::remove() and calls it before deleteValue — and crucially computes the ValueKey before cloneValue mutates the Value, so the correct entry is purged.
Overview
Impact:
Description: Visiting a maliciously crafted website may leak sensitive data
Researchers: Mohit Negi
Background
Advisory: Apple Security Advisory
| Attribute | Value |
|---|---|
| CVE | CVE-2026-64778 |
| Bugzilla | 322124 |
| Component | WebKit |
| Bug Class | LogicError |
| Severity | medium |
| Advisory | Apple Advisory |
Fix not public. The WebKit fix for this bug is embargoed or not yet disclosed. The bug ID is confirmed from the Apple advisory, but no public commit references it.
Root Cause Analysis
The issue was addressed with improved checks.
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: Shubham Chaskar, Tommy DeVoss from Braze Security Team (@thedawgyg)
Background
Advisory: Apple Security Advisory
| Attribute | Value |
|---|---|
| CVE | CVE-2026-64779 |
| Bugzilla | 321485 |
| Component | WebKit |
| Bug Class | LogicError |
| Severity | medium |
| Advisory | Apple Advisory |
Fix not public. The WebKit fix for this bug is embargoed or not yet disclosed. The bug ID is confirmed from the Apple advisory, but no public commit references it.
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: OpenAI Codex Security - Amy Burnett
Key insight. A misspelled setter (setUsessSIMD) and an empty notifyFunctionUsesSIMD meant B3 was never told a WASM function uses SIMD, so it miscompiled the vector code; wiring notifyFunctionUsesSIMD to setUsesSIMD() fixes it.
Patch Details
diff --git a/JSTests/wasm/stress/inline-wasm-simd-into-non-simd.js b/JSTests/wasm/stress/inline-wasm-simd-into-non-simd.js
new file mode 100644
index 000000000000..20ba8b3113d6
--- /dev/null
+++ b/JSTests/wasm/stress/inline-wasm-simd-into-non-simd.js
@@ -0,0 +1,54 @@
+load("../libwabt.js");
+
+const NUM_REFS = 49;
+
+let wabt = await WabtModule();
+
+let refParams = "", refResults = "", pushResults = "";
+
+for (let i = 1; i <= NUM_REFS; i++) {
+ refParams += " externref";
+ refResults += " externref";
+ pushResults += `(local.get ${i})\n`;
+}
+
+const wat = `
+(module
+ (tag $T (param i64 i64 i64 v128))
+ (func $B (param i64)
+ (throw $T (i64.const 0) (i64.const 0) (i64.const 0)
+ (i64x2.splat (local.get 0))))
+ (func $A (export "A") (param i64${refParams}) (result${refResults})
+ (try
+ (do (call $B (local.get 0)))
+ (catch_all))
+ ${pushResults}
+ )
+)`;
+
+const bin = wabt.parseWat("filenamesAreCool", wat, { simd: true, exceptions: true, reference_types: true, multi_value: true }).toBinary({}).buffer;
+const inst = new WebAssembly.Instance(new WebAssembly.Module(bin), {});
+const A = inst.exports.A;
+
+const sentinel = { marker: "SENTINEL" };
+const args = [0xan];
+for (let i = 0; i < NUM_REFS; i++)
+ args.push(sentinel);
+
+// Tier $A up to OMG.
+let last;
+for (let i = 0; i < testLoopCount; i++)
+ last = A.apply(null, args);
+
+let bad = -1;
+
+for (let k = 0; k < NUM_REFS; k++) {
+ if (last[k] !== sentinel) {
+ bad = k;
+ break;
+ }
+}
+
+if (bad >= 0) {
+ throw new Error("usesSIMD desync");
+}
diff --git a/Source/JavaScriptCore/b3/B3Procedure.cpp b/Source/JavaScriptCore/b3/B3Procedure.cpp
index 6f0d8bb8b466..1b1c1bf6d036 100644
--- a/Source/JavaScriptCore/b3/B3Procedure.cpp
+++ b/Source/JavaScriptCore/b3/B3Procedure.cpp
@@ -58,7 +58,7 @@ Procedure::Procedure(bool usesSIMD)
, m_heaps(makeUniqueRef<AbstractHeapRepository>())
{
if (usesSIMD)
- setUsessSIMD();
+ setUsesSIMD();
// Initialize all our fields before constructing Air::Code since
// it looks into our fields.
m_code = std::unique_ptr<Air::Code>(new Air::Code(*this));
diff --git a/Source/JavaScriptCore/b3/B3Procedure.h b/Source/JavaScriptCore/b3/B3Procedure.h
index e25a2ef198c7..47986dcdebbe 100644
--- a/Source/JavaScriptCore/b3/B3Procedure.h
+++ b/Source/JavaScriptCore/b3/B3Procedure.h
@@ -292,7 +292,7 @@ public:
bool shouldDumpIR() const { return m_shouldDumpIR; }
JS_EXPORT_PRIVATE void NODELETE setShouldDumpIR();
- void setUsessSIMD()
+ void setUsesSIMD()
{
RELEASE_ASSERT(Options::useWasmSIMD());
m_usesSIMD = true;
diff --git a/Source/JavaScriptCore/wasm/WasmOMGIRGenerator.cpp b/Source/JavaScriptCore/wasm/WasmOMGIRGenerator.cpp
index 05391ae63e35..e36dd2043b4e 100644
--- a/Source/JavaScriptCore/wasm/WasmOMGIRGenerator.cpp
+++ b/Source/JavaScriptCore/wasm/WasmOMGIRGenerator.cpp
@@ -497,7 +497,13 @@ public:
}
// SIMD
- void NODELETE notifyFunctionUsesSIMD() { ASSERT(m_info.usesSIMD(m_functionIndex)); }
+ bool NODELETE usesSIMD() const { return m_info.usesSIMD(m_functionIndex); }
+ void NODELETE notifyFunctionUsesSIMD()
+ {
+ ASSERT(m_info.usesSIMD(m_functionIndex));
+ m_proc.setUsesSIMD();
+ }
+
[[nodiscard]] PartialResult addSIMDLoad(ExpressionType pointer, uint64_t offset, ExpressionType& result, uint8_t memoryIndex);
[[nodiscard]] PartialResult addSIMDStore(ExpressionType value, ExpressionType pointer, uint64_t offset, uint8_t memoryIndex);
[[nodiscard]] PartialResult addSIMDSplat(SIMDLane, ExpressionType scalar, ExpressionType& result);
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: Thomas Guillem
Background
Advisory: Apple Security Advisory
| Attribute | Value |
|---|---|
| CVE | CVE-2026-64781 |
| Bugzilla | 321484 |
| Component | WebKit |
| Bug Class | LogicError |
| Severity | medium |
| Advisory | Apple Advisory |
Fix not public. The WebKit fix for this bug is embargoed or not yet disclosed. The bug ID is confirmed from the Apple advisory, but no public commit references it.
Root Cause Analysis
The issue was addressed with improved input validation.
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: Seonwook Kim, Shubham Chaskar, lattice, Josef Korbel
Background
Advisory: Apple Security Advisory
| Attribute | Value |
|---|---|
| CVE | CVE-2026-64782 |
| Bugzilla | 321480 |
| Component | WebKit |
| Bug Class | LogicError |
| Severity | medium |
| Advisory | Apple Advisory |
Fix not public. The WebKit fix for this bug is embargoed or not yet disclosed. The bug ID is confirmed from the Apple advisory, but no public commit references it.
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: Janggoon Lee of Out of Bounds, OpenAI Codex Security - Amy Burnett
Key insight. Size accounting for the metadata table used raw unsigned 32-bit arithmetic, so a function with tens of millions of metadata entries overflowed the total-size computation and under-allocated. The fix reworks the math with CheckedUint32 and rejects the compile (RangeError) on overflow rather than proceeding with a corrupt table.
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process termination
Researchers: 杉山 壮太, Shubham Chaskar
Key insight. Trusted Types introduced a new synchronous JavaScript re-entrancy point (the default policy callback) at existing HTML sinks, but the callers still passed a bare, unprotected contextDocument() across it — the fix is to protect() the document at each sink so the long-standing ‘hold a raw Document across a call that can run script’ pattern is made safe.
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: OpenAI Codex Security - Amy Burnett
Key insight. IRO eliminated checks based on a proven range but did not keep alive the nodes that proved the range, so DCE could delete them and leave an unchecked op without its guarantee; pinning the proof producers with NodeMustGenerate preserves the runtime condition.
Patch Details
diff --git a/JSTests/stress/arith-abs-checked-input-range.js b/JSTests/stress/arith-abs-checked-input-range.js
new file mode 100644
index 000000000000..09b95fa54ac8
--- /dev/null
+++ b/JSTests/stress/arith-abs-checked-input-range.js
@@ -0,0 +1,42 @@
+let enteredFirstBranch = 0;
+let enteredBoundsBranch = 0;
+let lastIndex = 0;
+
+function probe(x)
+{
+ const target = new Uint8Array(new ArrayBuffer(32));
+
+ let dead = x;
+ dead++;
+ dead++;
+
+ const first = Math.abs(x + 2);
+ if (first < 1) {
+ enteredFirstBranch++;
+ const index = first + 16;
+ lastIndex = index;
+ if (index < target.length) {
+ enteredBoundsBranch++;
+ return target[index];
+ }
+ }
+ return 0;
+}
+noInline(probe);
+
+for (let i = 0; i < 30000; ++i)
+ probe(-2);
+
+enteredFirstBranch = 0;
+enteredBoundsBranch = 0;
+lastIndex = 0;
+
+const result = probe(2147483646);
+if (result !== 0)
+ throw new Error(`Bad result: ${result}`);
+if (enteredFirstBranch)
+ throw new Error(`Entered first branch: ${enteredFirstBranch}`);
+if (enteredBoundsBranch)
+ throw new Error(`Entered bounds branch: ${enteredBoundsBranch}`);
+if (lastIndex)
+ throw new Error(`Computed wrapped index: ${lastIndex}`);
diff --git a/Source/JavaScriptCore/dfg/DFGIntegerRangeOptimizationPhase.cpp b/Source/JavaScriptCore/dfg/DFGIntegerRangeOptimizationPhase.cpp
index 552c0d8c1a16..48bb3950f815 100644
--- a/Source/JavaScriptCore/dfg/DFGIntegerRangeOptimizationPhase.cpp
+++ b/Source/JavaScriptCore/dfg/DFGIntegerRangeOptimizationPhase.cpp
@@ -1021,24 +1021,62 @@ public:
{
}
- std::optional<std::tuple<int32_t, int32_t>> NODELETE rangeFor(Node* node)
+ struct RangeBound {
+ int32_t value;
+ const Relationship* proof { nullptr };
+ };
+
+ std::optional<std::tuple<RangeBound, RangeBound>> NODELETE rangeFor(Node* node)
{
if (node->isInt32Constant()) {
int32_t value = node->asInt32();
- return std::tuple { value, value };
+ return std::tuple { RangeBound { value }, RangeBound { value } };
}
auto iter = m_relationships.find(node);
if (iter == m_relationships.end())
return std::nullopt;
- int32_t minValue = std::numeric_limits<int32_t>::min();
- int32_t maxValue = std::numeric_limits<int32_t>::max();
- for (Relationship relationship : iter->value) {
- minValue = WTF::max(minValue, relationship.minValueOfLeft());
- maxValue = WTF::min(maxValue, relationship.maxValueOfLeft());
+ RangeBound minBound { std::numeric_limits<int32_t>::min() };
+ RangeBound maxBound { std::numeric_limits<int32_t>::max() };
+ for (const Relationship& relationship : iter->value) {
+ int32_t candidateMin = relationship.minValueOfLeft();
+ if (candidateMin > minBound.value) {
+ minBound.value = candidateMin;
+ minBound.proof = &relationship;
+ }
+ int32_t candidateMax = relationship.maxValueOfLeft();
+ if (candidateMax < maxBound.value) {
+ maxBound.value = candidateMax;
+ maxBound.proof = &relationship;
+ }
}
- return std::tuple { minValue, maxValue };
+ return std::tuple { minBound, maxBound };
+ }
+
+ // Pin the upstream nodes referenced by the proof relationships behind one
+ // or more range bounds. Call before flipping a checked op to
+ // Arith::Unchecked. Prevents later phases (DCE, etc.) from removing the
+ // producers IRO consulted.
+ //
+ // FIXME: NodeMustGenerate is sticky. If the IRO-unmarked consumer later
+ // dies, its pinned dependency stays alive uselessly. Switch to
+ // reference-counted effect edges (from the unmarked node to each pinned
+ // producer, counted by DCE) so the pin self-cleans when the consumer is removed.
+ template<typename... Bounds>
+ void pinRangeBounds(const Bounds&... bounds)
+ {
+ (pinRangeBoundProof(bounds), ...);
+ }
+
+ void pinRangeBoundProof(const RangeBound& bound)
+ {
+ if (!bound.proof)
+ return;
+ if (Node* right = bound.proof->right().node(); right && !right->isConstant())
+ right->mergeFlags(NodeMustGenerate);
+ if (Node* left = bound.proof->left().node(); left && !left->isConstant())
+ left->mergeFlags(NodeMustGenerate);
}
// Be careful: do not use this to infer a relationship that will not be pruned later,
@@ -1314,7 +1352,9 @@ public:
auto range = rangeFor(node->child1().node());
if (!range)
break;
- auto [minValue, maxValue] = range.value();
+ auto [minBound, maxBound] = range.value();
+ int32_t minValue = minBound.value;
+ int32_t maxValue = maxBound.value;
executeNode(block->at(nodeIndex));
@@ -1326,12 +1366,15 @@ public:
bool absIsUnchecked = !shouldCheckOverflow(node->arithMode());
if (maxValue < 0 || (absIsUnchecked && maxValue <= 0)) {
node->convertToArithNegate();
- if (absIsUnchecked || minValue > std::numeric_limits<int>::min())
+ if (absIsUnchecked || minValue > std::numeric_limits<int>::min()) {
+ pinRangeBounds(minBound, maxBound);
node->setArithMode(Arith::Unchecked);
+ }
changed = true;
continue;
}
if (minValue > std::numeric_limits<int>::min()) {
+ pinRangeBounds(minBound);
node->setArithMode(Arith::Unchecked);
changed = true;
continue;
@@ -1348,12 +1391,16 @@ public:
auto leftRange = rangeFor(node->child1().node());
if (!leftRange)
break;
- auto [leftMinValue, leftMaxValue] = leftRange.value();
+ auto [leftMin, leftMax] = leftRange.value();
+ int32_t leftMinValue = leftMin.value;
+ int32_t leftMaxValue = leftMax.value;
auto rightRange = rangeFor(node->child2().node());
if (!rightRange)
break;
- auto [rightMinValue, rightMaxValue] = rightRange.value();
+ auto [rightMin, rightMax] = rightRange.value();
+ int32_t rightMinValue = rightMin.value;
+ int32_t rightMaxValue = rightMax.value;
dataLogLnIf(DFGIntegerRangeOptimizationPhaseInternal::verbose, " leftMinValue = ", leftMinValue, ", leftMaxValue = ", leftMaxValue, ", rightMinValue = ", rightMinValue, ", rightMaxValue = ", rightMaxValue);
@@ -1371,6 +1418,7 @@ public:
dataLogLnIf(DFGIntegerRangeOptimizationPhaseInternal::verbose, " It's in bounds.");
+ pinRangeBounds(leftMin, leftMax, rightMin, rightMax);
executeNode(block->at(nodeIndex));
node->setArithMode(Arith::Unchecked);
changed = true;
@@ -1386,12 +1434,16 @@ public:
auto leftRange = rangeFor(node->child1().node());
if (!leftRange)
break;
- auto [leftMinValue, leftMaxValue] = leftRange.value();
+ auto [leftMin, leftMax] = leftRange.value();
+ int32_t leftMinValue = leftMin.value;
+ int32_t leftMaxValue = leftMax.value;
auto rightRange = rangeFor(node->child2().node());
if (!rightRange)
break;
- auto [rightMinValue, rightMaxValue] = rightRange.value();
+ auto [rightMin, rightMax] = rightRange.value();
+ int32_t rightMinValue = rightMin.value;
+ int32_t rightMaxValue = rightMax.value;
dataLogLnIf(DFGIntegerRangeOptimizationPhaseInternal::verbose, " leftMinValue = ", leftMinValue, ", leftMaxValue = ", leftMaxValue, ", rightMinValue = ", rightMinValue, ", rightMaxValue = ", rightMaxValue);
@@ -1409,6 +1461,7 @@ public:
dataLogLnIf(DFGIntegerRangeOptimizationPhaseInternal::verbose, " It's in bounds.");
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: OpenAI Codex Security - Amy Burnett
Key insight. endBlock forced the declared signature result type only for ’else’, so try/catch (and other structured control) ends accepted a subtype fallthrough; forcing the signature type for all of them prevents smuggling a narrower/mistyped value.
Patch Details
diff --git a/JSTests/wasm/regress/try-catch-result-type.js b/JSTests/wasm/regress/try-catch-result-type.js
new file mode 100644
index 000000000000..6d4c8a064ca4
--- /dev/null
+++ b/JSTests/wasm/regress/try-catch-result-type.js
@@ -0,0 +1,23 @@
+import * as assert from '../assert.js';
+
+const bytes = new Uint8Array([
+ 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
+ 0x01, 0x8c, 0x80, 0x80, 0x80, 0x00,
+ 0x03, 0x5e, 0x7e, 0x01, 0x60, 0x00, 0x00, 0x60, 0x01, 0x6f, 0x01, 0x7f,
+ 0x03, 0x82, 0x80, 0x80, 0x80, 0x00, 0x01, 0x02,
+ 0x0d, 0x83, 0x80, 0x80, 0x80, 0x00, 0x01, 0x00, 0x01,
+ 0x07, 0x88, 0x80, 0x80, 0x80, 0x00, 0x01, 0x04, 0x6d, 0x61, 0x69, 0x6e, 0x00, 0x00,
+ 0x0a, 0x99, 0x80, 0x80, 0x80, 0x00, 0x01, 0x93, 0x80, 0x80, 0x80, 0x00,
+ 0x00, 0x06, 0x6e, 0x20, 0x00, 0xfb, 0x1a, 0x07, 0x00,
+ 0x41, 0x01, 0xfb, 0x07, 0x00, 0x0b, 0xfb, 0x14, 0x6a, 0x0b,
+]);
+
+const instance = new WebAssembly.Instance(new WebAssembly.Module(bytes));
+
+const storage = new ArrayBuffer(8);
+const view = new DataView(storage);
+view.setBigUint64(0, 0x4141414141414141n - (1n << 49n), true);
+const nonCellExternref = view.getFloat64(0, true);
+
+for (let i = 0; i < wasmTestLoopCount; ++i)
+ assert.eq(instance.exports.main(nonCellExternref), 0);
\ No newline at end of file
diff --git a/Source/JavaScriptCore/wasm/WasmFunctionParser.h b/Source/JavaScriptCore/wasm/WasmFunctionParser.h
index 756c219bc5ca..4f4cf92ad41f 100644
--- a/Source/JavaScriptCore/wasm/WasmFunctionParser.h
+++ b/Source/JavaScriptCore/wasm/WasmFunctionParser.h
@@ -3855,13 +3855,14 @@ FOR_EACH_WASM_MEMORY_STORE_OP(CREATE_CASE)
m_expressionStack.shrink(m_currentStackBegin);
m_expressionStack.append(data.elseBlockStack.span());
}
- // When ending an 'if'/'else', including a synthetic 'else' added right above,
- // the spec requires the output type of 'if' to be the type from the signature.
- const bool shouldForceSignature = ControlType::isElse(data.controlData);
+
// FIXME: endBlock may modify the expressionStack slice for the result of the block.
// That's a little too effectful but we don't have a better API right now.
// see: https://bugs.webkit.org/show_bug.cgi?id=164353
- WASM_FAIL_IF_HELPER_FAILS(checkExpressionStack(data.controlData, shouldForceSignature));
+
+ // The spec requires the output type of a structured control instruction to be
+ // the result type from its signature, even when the fallthrough value is a subtype.
+ WASM_FAIL_IF_HELPER_FAILS(checkExpressionStack(data.controlData, true));
const uint32_t parentBegin = parentEntryBegin();
auto enclosedStack = m_expressionStack.mutableSpan().subspan(parentBegin);
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: OpenAI Codex Security - Amy Burnett
Key insight. B3’s reverse CFG omitted exit-less back-edge sources from the synthetic root, so an exiting latch falsely post-dominated the loop header, letting LICM hoist a control-dependent read out of the loop; adding every latch as a reverse-root successor restores correct post-dominance.
Patch Details
diff --git a/JSTests/wasm/gc/backwards-graph-multi-backedge-licm.js b/JSTests/wasm/gc/backwards-graph-multi-backedge-licm.js
new file mode 100644
index 000000000000..b5c92115ab1d
--- /dev/null
+++ b/JSTests/wasm/gc/backwards-graph-multi-backedge-licm.js
@@ -0,0 +1,109 @@
+//@ runDefaultWasm("--useConcurrentJIT=0", "--thresholdForBBQOptimizeAfterWarmUp=0", "--thresholdForBBQOptimizeSoon=0", "--thresholdForOMGOptimizeAfterWarmUp=0", "--thresholdForOMGOptimizeSoon=0")
+
+load("wast.js", "caller relative");
+
+const moduleBytes = WebAssemblyText.encode(`
+(module
+ (type $sink (struct (field i64)))
+ (type $probe
+ (struct
+ (field i64)
+ (field (ref $sink))
+ (field (ref $sink))
+ (field (ref $sink))))
+ (type $small
+ (struct
+ (field i64)
+ (field i64)
+ (field i64)
+ (field i64)))
+ (global $keep (mut i32) (i32.const 0))
+
+ (func (export "makeSink") (param $value i64) (result (ref $sink))
+ (struct.new $sink (local.get $value)))
+
+ (func (export "makeProbe")
+ (param $value i64)
+ (param $sink (ref $sink))
+ (result (ref $probe))
+ (struct.new $probe
+ (local.get $value)
+ (local.get $sink)
+ (local.get $sink)
+ (local.get $sink)))
+
+ (func (export "makeSmall") (result (ref $small))
+ (struct.new $small
+ (i64.const 0)
+ (i64.const 0)
+ (i64.const 0)
+ (i64.const 0)))
+
+ (func (export "readAfterCast")
+ (param $source anyref)
+ (param $valid (ref $sink))
+ (param $bit i64)
+ (param $forceBit i32)
+ (param $useLoaded i32)
+ (param $lateLoop i32)
+ (result i64)
+ (local $probeLocal (ref $probe))
+ (loop $loop (result i64)
+ (block $castSucceeded (result (ref $probe))
+ (br_on_cast $castSucceeded
+ (ref null any)
+ (ref $probe)
+ (local.get $source))
+ (global.set $keep (i32.const 1))
+ (br $loop))
+ (local.set $probeLocal)
+
+ (struct.get $probe 2 (local.get $probeLocal))
+ (struct.get $probe 3 (local.get $probeLocal))
+ (struct.get $probe 2 (local.get $probeLocal))
+ (local.get $valid)
+ (struct.get $probe 1 (local.get $probeLocal))
+ (struct.get $sink 0)
+ (local.get $bit)
+ i64.shr_u
+ i32.wrap_i64
+ i32.const 1
+ i32.and
+ (local.get $forceBit)
+ (local.get $useLoaded)
+ (select (result i32))
+ i32.eqz
+ (select (result (ref $sink)))
+ (struct.get $sink 0)
+ i64.const 1
+ i64.and
+ i32.wrap_i64
+ (select (result (ref $sink)))
+ (struct.get $sink 0)
+ (br_if $loop (local.get $lateLoop))))
+)
+`);
+
+const { makeProbe, makeSink, makeSmall, readAfterCast } =
+ new WebAssembly.Instance(new WebAssembly.Module(moduleBytes)).exports;
+const sink = makeSink(0x5152535455565758n);
+const probe = makeProbe(0x1111111111111111n, sink);
+
+for (let iteration = 0; iteration < 1000; ++iteration)
+ readAfterCast(probe, sink, 0n, 0, 1, 0);
+
+// Correct semantics: cast failure loops forever before any control-dependent
+// struct.get executes. OMG-compiled Wasm loops have no watchdog poll (rdar://103455312), so bound
+// the test by exiting from a helper thread once the main thread has had time to
+// reach the loop.
+$.agent.start(`
+ $.agent.sleep(10); // 10 ms
+ quit();
+`);
+
+try {
+ readAfterCast(makeSmall(), sink, 0n, 0, 1, 0);
+ throw new Error("readAfterCast on a failed cast should not return");
+} catch (e) {
+ throw new Error("B3 LICM hoisted control-dependent struct.get past br_on_cast: " + e);
+}
diff --git a/Source/JavaScriptCore/b3/B3Procedure.h b/Source/JavaScriptCore/b3/B3Procedure.h
index 34146786d868..e25a2ef198c7 100644
--- a/Source/JavaScriptCore/b3/B3Procedure.h
+++ b/Source/JavaScriptCore/b3/B3Procedure.h
@@ -201,7 +201,7 @@ public:
Dominators& dominators();
JS_EXPORT_PRIVATE NaturalLoops& naturalLoops();
BackwardsCFG& backwardsCFG();
- BackwardsDominators& backwardsDominators();
+ JS_EXPORT_PRIVATE BackwardsDominators& backwardsDominators();
void addFastConstant(const ValueKey&);
bool NODELETE isFastConstant(const ValueKey&);
diff --git a/Source/JavaScriptCore/b3/testb3.h b/Source/JavaScriptCore/b3/testb3.h
index 1db3114220f6..cce642296502 100644
--- a/Source/JavaScriptCore/b3/testb3.h
+++ b/Source/JavaScriptCore/b3/testb3.h
@@ -1292,6 +1292,7 @@ void testShuffleDoesntTrashCalleeSaves();
void testDemotePatchpointTerminal();
void testReportUsedRegistersLateUseFollowedByEarlyDefDoesNotMarkUseAsDead();
void testInfiniteLoopDoesntCauseBadHoisting();
+void testBackwardsDominatorsWithMultipleBackEdges();
void testDivImmArgFloat(float, float);
void testDivImmsFloat(float, float);
void testModArgDouble(double);
diff --git a/Source/JavaScriptCore/b3/testb3_1.cpp b/Source/JavaScriptCore/b3/testb3_1.cpp
index fe1d75ae50c2..bc038f769ecf 100644
--- a/Source/JavaScriptCore/b3/testb3_1.cpp
+++ b/Source/JavaScriptCore/b3/testb3_1.cpp
@@ -1190,6 +1190,7 @@ void run(const TestConfig* config)
RUN(testLoopWithMultipleHeaderEdges());
RUN(testInfiniteLoopDoesntCauseBadHoisting());
+ RUN(testBackwardsDominatorsWithMultipleBackEdges());
RUN(testFloatMaxMin());
RUN(testDoubleMaxMin());
diff --git a/Source/JavaScriptCore/b3/testb3_7.cpp b/Source/JavaScriptCore/b3/testb3_7.cpp
index dd6cad5ca71c..eded060da2be 100644
--- a/Source/JavaScriptCore/b3/testb3_7.cpp
+++ b/Source/JavaScriptCore/b3/testb3_7.cpp
@@ -25,6 +25,7 @@
#include "config.h"
#include "testb3.h"
+#include "B3BackwardsDominators.h"
#include <wtf/WasmSIMD128.h>
WTF_ALLOW_UNSAFE_BUFFER_USAGE_BEGIN
@@ -2309,6 +2310,70 @@ void testInfiniteLoopDoesntCauseBadHoisting()
invoke<void>(*code, static_cast<intptr_t>(55)); // Shouldn't crash dereferncing 55.
}
+void testBackwardsDominatorsWithMultipleBackEdges()
+{
+ // A loop whose header has two latches (back-edge sources), where only one
+ // of them also exits the loop:
+ //
+ // root --> header
+ // header --> success | failure (branch)
+ // success --> header (back edge) | exit (branch) // latch that can exit
+ // failure --> header (back edge) // latch with no exit
+ // exit --> Return // the only terminal
+ //
+ // `success` and `failure` are both back-edge sources. The `failure` latch
+ // has no exit of its own, so control that keeps taking it loops forever.
+ // BackwardsGraph treats that as a form of terminality: it computes
+ // post-dominators by reversing the CFG and giving the synthetic reverse
+ // root a successor for every terminal and every back-edge source.
+ //
+ // The property B3 LICM (and the control-equivalence consumers) rely on is
+ // the post-dominator relation: `success` must NOT post-dominate the header
+ // or the loop entry, because control can stay in the loop forever via the
+ // exit-less `failure` latch without ever reaching `success`. If `failure`
+ // is missing from the reverse root's successors, the header is reachable in
+ // the reverse CFG only through `success`, so `success` falsely
+ // post-dominates the header (and `root`) -- the false relationship that
+ // would let LICM hoist a control-dependent read out of the loop.
+ Procedure proc;
+ BasicBlock* root = proc.addBlock();
+ BasicBlock* header = proc.addBlock();
+ BasicBlock* success = proc.addBlock();
+ BasicBlock* failure = proc.addBlock();
+ BasicBlock* exit = proc.addBlock();
+ auto arguments = cCallArgumentValues<intptr_t>(proc, root);
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: OpenAI Codex Security - Amy Burnett
Key insight. Select specialization cloned values without checking cloneability, and in-try call patchpoints (which carry a per-call-site exception stackmap) were cloneable, so cloning them aliased two call sites to one stackmap; forbidding cloning of those patchpoints and gating specialization on isCloningForbidden fixes it.
Patch Details
diff --git a/JSTests/wasm/stress/omg-reduce-strength-select-exception-stackmap.js b/JSTests/wasm/stress/omg-reduce-strength-select-exception-stackmap.js
new file mode 100644
index 000000000000..23211b1e2648
--- /dev/null
+++ b/JSTests/wasm/stress/omg-reduce-strength-select-exception-stackmap.js
@@ -0,0 +1,151 @@
+// The module is hand-assembled as raw bytes rather than written in WAT because
+// the current in-tree WAT assemblers do not support both GC and exceptions.
+
+// --- WebAssembly encoding helpers ------------------------------------------------
+
+// Unsigned LEB128.
+function u32(value)
+{
+ const result = [];
+ do {
+ let byte = value & 0x7f;
+ value >>>= 7;
+ if (value)
+ byte |= 0x80;
+ result.push(byte);
+ } while (value);
+ return result;
+}
+
+// A name/vec(byte): length-prefixed UTF-8 bytes.
+function str(value)
+{
+ const result = [];
+ for (let i = 0; i < value.length; ++i)
+ result.push(value.charCodeAt(i));
+ return [...u32(result.length), ...result];
+}
+
+// section(id, payload) = id, size(payload), payload.
+function section(id, payload)
+{
+ return [id, ...u32(payload.length), ...payload];
+}
+
+// A function body: vec(locals), code, 0x0b(end-of-function). `localGroups` is a
+// list of [count, valtype] pairs; `code` is the instruction stream.
+function body(localGroups, code)
+{
+ const payload = [...u32(localGroups.length)];
+ for (const [count, type] of localGroups)
+ payload.push(...u32(count), type);
+ payload.push(...code, 0x0b); // 0x0b = end (of function)
+ return [...u32(payload.length), ...payload];
+}
+
+const localGet = (index) => [0x20, ...u32(index)]; // 0x20 = local.get
+const localSet = (index) => [0x21, ...u32(index)]; // 0x21 = local.set
+const bitsCount = 24;
+
+// valtype bytes: 0x7f = i32, 0x7e = i64, 0x6f = externref. Section ids: 1 type,
+// 2 import, 3 function, 7 export, 10 code, 13 tag.
+
+function makeModule()
+{
+ // --- Type section (id 1): four function types ------------------------------
+ const helperParams = [...new Array(bitsCount).fill(0x7e), 0x6f]; // 24 x i64, then externref
+ const typeSection = [
+ ...u32(4), // 4 types
+ 0x60, 0x00, 0x01, 0x7f, // type 0: () -> i32 ($thrower)
+ 0x60, ...u32(helperParams.length), ...helperParams, 0x01, 0x7f, // type 1: (i64 x24, externref) -> i32 ($helper)
+ 0x60, 0x01, 0x7f, 0x01, 0x6f, // type 2: (i32) -> externref ($target)
+ 0x60, 0x00, 0x00, // type 3: () -> () (tag type)
+ ];
+
+ // --- Import section (id 2): one function + 25 mutable globals ---------------
+ const imports = [
+ [...str("m"), ...str("thrower"), 0x00, ...u32(0)], // func "m"."thrower" : type 0
+ [...str("m"), ...str("object"), 0x03, 0x6f, 0x01], // global "m"."object" : externref, mutable
+ ];
+ for (let index = 0; index < bitsCount; ++index)
+ imports.push([...str("m"), ...str(`bits${index}`), 0x03, 0x7e, 0x01]); // global "m"."bitsN" : i64, mutable
+ const importSection = [...u32(imports.length), ...imports.flat()];
+
+ // --- Function section (id 3): the two defined functions ---------------------
+ // Imported funcs occupy index 0 ($thrower); defined funcs follow:
+ // func 1 = $helper (type 1), func 2 = $target (type 2).
+ const functionSection = [...u32(2), ...u32(1), ...u32(2)];
+
+ // --- Tag section (id 13): one exception tag of type 3 -----------------------
+ const tagSection = [...u32(1), 0x00, ...u32(3)]; // 1 tag, attribute 0 (exception), type 3
+
+ // --- Export section (id 7): export $target ----------------------------------
+ const exportSection = [...u32(1), ...str("target"), 0x00, ...u32(2)]; // "target" = func 2
+
+ // --- Code section (id 10): bodies for $helper and $target -------------------
+
+ // $helper: padding nops keep it above the inlining threshold, then it calls the
+ // imported JS thrower (which always throws).
+ const helperBody = body([], [
+ ...new Array(600).fill(0x01), // nop x600 (0x01 = nop)
+ 0x10, 0x00, // call 0 ($thrower)
+ ]);
+
+ // $target: the trigger. One externref local ($live = local 1; local 0 = param).
+ const targetBody = body([[1, 0x6f]], [
+ 0x06, 0x6f, // try (result externref)
+ ...new Array(bitsCount).fill(0).flatMap((_, index) => [0x23, ...u32(1 + index)]), // global.get $bits0 .. $bits23 (globals 1..24)
+ 0xd0, 0x6f, // ref.null extern (0xd0 ref.null, 0x6f extern; constant select arm)
+ 0x23, 0x00, // global.get 0 ($object, the other select arm)
+ ...localGet(0), // local.get 0 ($predicate, the select condition)
+ 0x1c, 0x01, 0x6f, // select (result externref) (0x1c typed-select, 1 result type, externref)
+ ...localSet(1), // local.set 1 ($live = select result)
+ ...localGet(1), // local.get 1 ($live, passed as the call's externref arg)
+ 0x10, 0x01, // call 1 ($helper; exception-capable call cloned by specializeSelect)
+ 0x1a, // drop (discard $helper's i32 result)
+ ...localGet(1), // local.get 1 ($live)
+ 0xd4, // ref.as_non_null (0xd4; the Check whose Select specializeSelect rewrites)
+ 0x1a, // drop
+ 0xd0, 0x6f, // ref.null extern (normal-path try result; unreachable, $helper always throws)
+ 0x19, // catch_all (0x19)
+ ...localGet(1), // local.get 1 ($live -> restored externref, the function result)
+ 0x0b, // end (of try)
+ ]);
+
+ const codeSection = [...u32(2), ...helperBody, ...targetBody]; // 2 function bodies
+
+ return new Uint8Array([
+ 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, // "\0asm", version 1
+ ...section(1, typeSection),
+ ...section(2, importSection),
+ ...section(3, functionSection),
+ ...section(13, tagSection),
+ ...section(7, exportSection),
+ ...section(10, codeSection),
+ ]);
+}
+
+// --- Imports: a throwing function, a marker externref, and 24 i64 sentinels ------
+
+const marker = { marker: 0x1227 };
+const raw42 = 0xfffe00000000002an;
+const imports = {
+ thrower() {
+ throw marker;
+ },
+ object: new WebAssembly.Global({ value: "externref", mutable: true }, marker),
+};
+for (let index = 0; index < bitsCount; ++index) {
+ imports[`bits${index}`] = new WebAssembly.Global(
+ { value: "i64", mutable: true },
+ BigInt.asIntN(64, raw42 + BigInt(index) * 0x100n));
+}
+
+const target = new WebAssembly.Instance(
+ new WebAssembly.Module(makeModule()), { m: imports }).exports.target;
+
+for (let iteration = 0; iteration < wasmTestLoopCount; ++iteration) {
+ const result = target(1);
+ if (result !== null)
+ throw new Error(`expected null catch restoration, got ${String(result)} at iteration ${iteration}`);
+}
diff --git a/Source/JavaScriptCore/b3/B3ReduceStrength.cpp b/Source/JavaScriptCore/b3/B3ReduceStrength.cpp
index 999cdda89af1..b587e0ddf97a 100644
--- a/Source/JavaScriptCore/b3/B3ReduceStrength.cpp
+++ b/Source/JavaScriptCore/b3/B3ReduceStrength.cpp
@@ -3292,7 +3292,23 @@ private:
&& (value->child(1)->isConstant() || value->child(2)->isConstant());
});
- if (select) {
+ if (!select)
+ break;
+
+ // All values between Select and Check must be cloneable.
+ bool canClone = true;
+ for (unsigned i = m_index; ; --i) {
+ Value* value = m_block->at(i);
+ if (value->kind().isCloningForbidden()) {
+ canClone = false;
+ break;
+ }
+ if (value == select)
+ break;
+ RELEASE_ASSERT(i); // Select should be found
+ }
+
+ if (canClone) {
specializeSelect(select);
m_didSpecializeSelect = true;
break;
diff --git a/Source/JavaScriptCore/wasm/WasmOMGIRGenerator.cpp b/Source/JavaScriptCore/wasm/WasmOMGIRGenerator.cpp
index 11e808ae5371..0b2b90bb8084 100644
--- a/Source/JavaScriptCore/wasm/WasmOMGIRGenerator.cpp
+++ b/Source/JavaScriptCore/wasm/WasmOMGIRGenerator.cpp
@@ -4990,6 +4990,8 @@ RefPtr<PatchpointExceptionHandle> OMGIRGenerator::preparePatchpointForExceptions
if (!mustSaveState)
return nullptr;
+ ASSERT(patch->kind().isCloningForbidden());
+
unsigned firstStackmapChildOffset = patch->numChildren();
unsigned firstStackmapParamOffset = firstStackmapChildOffset + m_proc.resultCount(patch->type());
@@ -5597,7 +5599,10 @@ auto OMGIRGenerator::createCallPatchpoint(BasicBlock* block, const RTT& signatur
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: OpenAI Codex Security - Amy Burnett
Key insight. RegExp compile paths refreshed only part of the metadata and asserted (compiled out in release) that the subpattern count was unchanged, so a soft-invalid pattern left stale capture metadata against a mismatched bytecode; consistently refreshing metadata and refusing to cache/constant-fold invalid RegExps fixes it.
Patch Details
diff --git a/JSTests/stress/new-regexp-untyped-soft-invalid-stale-captures.js b/JSTests/stress/new-regexp-untyped-soft-invalid-stale-captures.js
new file mode 100644
index 000000000000..9d7fba576e76
--- /dev/null
+++ b/JSTests/stress/new-regexp-untyped-soft-invalid-stale-captures.js
@@ -0,0 +1,52 @@
+//@ runDefault("--jitPolicyScale=0")
+
+const nesting = 34000;
+const callDepth = 8000;
+const iterations = testLoopCount;
+const stride = 1024;
+const emptyCaptures = 22;
+
+function atDepth(depth, callback) {
+ const keep = depth;
+ if (!depth)
+ return callback() + keep;
+ return atDepth(depth - 1, callback) + Number(keep === -1);
+}
+
+let regexpPayload = "";
+for (let i = 0; i < emptyCaptures; ++i)
+ regexpPayload += "()";
+regexpPayload += "(?=b*(a*))";
+regexpPayload += "(?:stablefinalread)?";
+
+const pattern = "(?:".repeat(nesting) + regexpPayload + ")".repeat(nesting);
+
+const pieces = [];
+for (let offset = 0; offset < pattern.length; offset += 8000)
+ pieces.push(JSON.stringify(pattern.slice(offset, offset + 8000)));
+const make = eval(`(function (run) { if (!run) return 1; return new RegExp(${pieces.join(" + ")}, ""); })`);
+
+try { make(true); } catch (e) { }
+fullGC();
+
+let exposed = null;
+try {
+ atDepth(callDepth, () => {
+ try { new RegExp(pattern, ""); } catch (e) { }
+ for (let i = 0; i < iterations && !exposed; ++i) {
+ make(false);
+ if ((i % stride) === stride - 1)
+ try { exposed = make(true); } catch (e) { }
+ }
+ if (!exposed)
+ try { exposed = make(true); } catch (e) { }
+ return 0;
+ });
+} catch (e) { }
+
+if (exposed) {
+ const subject = "a".repeat(113);
+ for (let i = 0; i < 6; ++i) {
+ try { exposed.test(subject); } catch (e) { }
+ }
+}
diff --git a/Source/JavaScriptCore/dfg/DFGStrengthReductionPhase.cpp b/Source/JavaScriptCore/dfg/DFGStrengthReductionPhase.cpp
index 78ca126c6509..42131b5c5028 100644
--- a/Source/JavaScriptCore/dfg/DFGStrengthReductionPhase.cpp
+++ b/Source/JavaScriptCore/dfg/DFGStrengthReductionPhase.cpp
@@ -716,6 +716,9 @@ private:
if (!regExp)
break;
+ if (!regExp->isValid())
+ break;
+
m_node->convertToNewRegExp(m_graph.freezeStrong(regExp), m_insertionSet.insertConstantForUse(m_nodeIndex, m_node->origin, jsNumber(0), UntypedUse));
m_changed = true;
break;
diff --git a/Source/JavaScriptCore/runtime/RegExp.cpp b/Source/JavaScriptCore/runtime/RegExp.cpp
index 2c636589f41e..5b2553b6d5fd 100644
--- a/Source/JavaScriptCore/runtime/RegExp.cpp
+++ b/Source/JavaScriptCore/runtime/RegExp.cpp
@@ -164,12 +164,18 @@ void RegExp::finishCreation(VM& vm)
return;
}
+ updateMetadataFromPattern(pattern);
+}
+
+void RegExp::updateMetadataFromPattern(Yarr::YarrPattern& pattern)
+{
m_atom = WTF::move(pattern.m_atom);
m_specificPattern = pattern.m_specificPattern;
m_numSubpatterns = pattern.m_numSubpatterns;
if (!pattern.m_captureGroupNames.isEmpty() || !pattern.m_namedGroupToParenIndices.isEmpty()) {
- m_rareData = makeUnique<RareData>();
+ if (!m_rareData)
+ m_rareData = makeUnique<RareData>();
m_rareData->m_numDuplicateNamedCaptureGroups = pattern.m_numDuplicateNamedCaptureGroups;
m_rareData->m_captureGroupNames.swap(pattern.m_captureGroupNames);
m_rareData->m_namedGroupToParenIndices.swap(pattern.m_namedGroupToParenIndices);
@@ -226,10 +232,7 @@ void RegExp::byteCodeCompileIfNecessary(VM* vm)
m_state = ParseError;
return;
}
- ASSERT(m_numSubpatterns == pattern.m_numSubpatterns);
-
- m_atom = WTF::move(pattern.m_atom);
- m_specificPattern = pattern.m_specificPattern;
+ updateMetadataFromPattern(pattern);
m_regExpBytecode = byteCodeCompilePattern(vm, pattern, m_constructionErrorCode);
if (!m_regExpBytecode) {
@@ -247,10 +250,7 @@ void RegExp::compile(VM* vm, Yarr::CharSize charSize, std::optional<StringView>
m_state = ParseError;
return;
}
- ASSERT(m_numSubpatterns == pattern.m_numSubpatterns);
-
- m_atom = WTF::move(pattern.m_atom);
- m_specificPattern = pattern.m_specificPattern;
+ updateMetadataFromPattern(pattern);
if (!hasCode()) {
ASSERT(m_state == NotCompiled);
@@ -315,10 +315,7 @@ void RegExp::compileMatchOnly(VM* vm, Yarr::CharSize charSize, std::optional<Str
m_state = ParseError;
return;
}
- ASSERT(m_numSubpatterns == pattern.m_numSubpatterns);
-
- m_atom = WTF::move(pattern.m_atom);
- m_specificPattern = pattern.m_specificPattern;
+ updateMetadataFromPattern(pattern);
if (!hasCode()) {
ASSERT(m_state == NotCompiled);
diff --git a/Source/JavaScriptCore/runtime/RegExp.h b/Source/JavaScriptCore/runtime/RegExp.h
index 328e1290eb6f..ade76d562bc5 100644
--- a/Source/JavaScriptCore/runtime/RegExp.h
+++ b/Source/JavaScriptCore/runtime/RegExp.h
@@ -35,6 +35,10 @@
namespace JSC {
+namespace Yarr {
+struct YarrPattern;
+}
+
struct RegExpRepresentation;
class VM;
@@ -176,6 +180,8 @@ private:
RegExp(VM&, const String&, OptionSet<Yarr::Flags>);
void finishCreation(VM&);
+ void updateMetadataFromPattern(Yarr::YarrPattern&);
+
static RegExp* createWithoutCaching(VM&, const String&, OptionSet<Yarr::Flags>);
enum RegExpState : uint8_t {
diff --git a/Source/JavaScriptCore/runtime/RegExpCache.cpp b/Source/JavaScriptCore/runtime/RegExpCache.cpp
index f7059ad07954..ac9e547c0e91 100644
--- a/Source/JavaScriptCore/runtime/RegExpCache.cpp
+++ b/Source/JavaScriptCore/runtime/RegExpCache.cpp
@@ -57,6 +57,9 @@ RegExp* RegExpCache::lookupOrCreate(VM& vm, const String& patternString, OptionS
vm.addRegExpToTrace(regExp);
#endif
+ if (!regExp->isValid())
+ return regExp;
+
{
Locker locker { m_lock };
weakAdd(m_weakCache, key, Weak<RegExp>(regExp, this));
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: Josef Korbel
Key insight. The IPInt interpreter reconstructed the ref-cast allowNull flag from a fixed bytecode offset (loadb 2[PC]) instead of validated metadata, so casts could use the wrong nullability; storing allowNull in RefTestCastMetadata and reading it from there fixes the type confusion.
Patch Details
diff --git a/JSTests/wasm/stress/br-on-cast-fail-overlong-leb128.js b/JSTests/wasm/stress/br-on-cast-fail-overlong-leb128.js
new file mode 100644
index 000000000000..751087b87498
--- /dev/null
+++ b/JSTests/wasm/stress/br-on-cast-fail-overlong-leb128.js
@@ -0,0 +1,75 @@
+"use strict";
+// Canonical and overlong LEB128 encodings of a GC sub-opcode must produce
+// identical execution semantics. This test checks that for br_on_cast_fail
+// with FLAGS=0x03 - divergence means IPInt is reading the flags byte from a
+// position the encoding moved.
+
+function uleb(n) { const o=[]; do { let b=n&0x7f; n>>>=7; if(n) b|=0x80; o.push(b); } while(n); return o; }
+function section(id, b) { return [id, ...uleb(b.length), ...b]; }
+function str(s) { return [s.length, ...Array.from(s, c => c.charCodeAt(0))]; }
+
+function buildModule(brOnCastFail) {
+ const typeSec = [
+ 0x03,
+ 0x5f, 0x00, // type 0: (struct)
+ 0x60, 0x00, 0x00, // type 1: () -> void
+ 0x60, 0x00, 0x01, 0x7f, // type 2: () -> i32
+ ];
+ const funcSec = [0x02, 0x01, 0x02];
+ // global 0: (mut (ref null any)) init = (struct.new_default 0)
+ const globalSec = [0x01, 0x63, 0x6e, 0x01, 0xfb, 0x01, 0x00, 0x0b];
+ const exportSec = [
+ 0x02,
+ ...str("poison"), 0x00, 0x00,
+ ...str("is_poisoned"), 0x00, 0x01,
+ ];
+ const poisonBody = [
+ 0x00,
+ 0x02, 0x63, 0x6e, // block (result (ref null any))
+ 0xd0, 0x6e, // ref.null any
+ ...brOnCastFail, // br_on_cast_fail 0 (FLAGS=0x03 in either encoding)
+ 0x1a, // drop (drops the (ref null 0) cast result)
+ 0xfb, 0x01, 0x00, // struct.new_default 0
+ 0x0c, 0x00, // br 0
+ 0x0b,
+ 0x24, 0x00, // global.set 0
+ 0x0b,
+ ];
+ const checkBody = [
+ 0x00,
+ 0x23, 0x00, // global.get 0
+ 0xd1, // ref.is_null
+ 0x0b,
+ ];
+ const codeSec = [
+ 0x02,
+ ...uleb(poisonBody.length), ...poisonBody,
+ ...uleb(checkBody.length), ...checkBody,
+ ];
+ return new Uint8Array([
+ 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
+ ...section(1, typeSec),
+ ...section(3, funcSec),
+ ...section(6, globalSec),
+ ...section(7, exportSec),
+ ...section(10, codeSec),
+ ]);
+}
+
+// prefix sub-op flags label ht1 ht2
+const CANONICAL = [0xfb, 0x19, 0x03, 0x00, 0x6e, 0x00];
+const OVERLONG = [0xfb, 0x99,0x80,0x00, 0x03, 0x00, 0x6e, 0x00];
+
+const a = new WebAssembly.Instance(new WebAssembly.Module(buildModule(CANONICAL)));
+const b = new WebAssembly.Instance(new WebAssembly.Module(buildModule(OVERLONG)));
+
+a.exports.poison();
+b.exports.poison();
+
+const rA = a.exports.is_poisoned();
+const rB = b.exports.is_poisoned();
+
+if (rA !== rB)
+ throw new Error("LEB encoding of GC sub-opcode affects br_on_cast_fail semantics: "
+ + "canonical=" + rA + " overlong=" + rB
+ + " — IPInt reads flags from PC at hardcoded offset");
diff --git a/JSTests/wasm/stress/br-on-cast-overlong-leb128.js b/JSTests/wasm/stress/br-on-cast-overlong-leb128.js
new file mode 100644
index 000000000000..acb7d7784641
--- /dev/null
+++ b/JSTests/wasm/stress/br-on-cast-overlong-leb128.js
@@ -0,0 +1,75 @@
+"use strict";
+// Canonical and overlong LEB128 encodings of a GC sub-opcode must produce
+// identical execution semantics. This test checks that for br_on_cast with
+// FLAGS=0x03 - divergence means IPInt is reading the flags byte from a
+// position the encoding moved.
+
+function uleb(n) { const o=[]; do { let b=n&0x7f; n>>>=7; if(n) b|=0x80; o.push(b); } while(n); return o; }
+function section(id, b) { return [id, ...uleb(b.length), ...b]; }
+function str(s) { return [s.length, ...Array.from(s, c => c.charCodeAt(0))]; }
+
+function buildModule(brOnCast) {
+ const typeSec = [
+ 0x03,
+ 0x5f, 0x00, // type 0: (struct)
+ 0x60, 0x00, 0x00, // type 1: () -> void
+ 0x60, 0x00, 0x01, 0x7f, // type 2: () -> i32
+ ];
+ const funcSec = [0x02, 0x01, 0x02];
+ // global 0: (mut (ref null 0)) init = (struct.new_default 0)
+ const globalSec = [0x01, 0x63, 0x00, 0x01, 0xfb, 0x01, 0x00, 0x0b];
+ const exportSec = [
+ 0x02,
+ ...str("poison"), 0x00, 0x00,
+ ...str("is_poisoned"), 0x00, 0x01,
+ ];
+ const poisonBody = [
+ 0x00,
+ 0x02, 0x63, 0x00, // block (result (ref null 0))
+ 0xd0, 0x6e, // ref.null any
+ ...brOnCast, // br_on_cast 0 (FLAGS=0x03 in either encoding)
+ 0x1a, // drop
+ 0xfb, 0x01, 0x00, // struct.new_default 0 (only reached if cast missed)
+ 0x0c, 0x00, // br 0
+ 0x0b,
+ 0x24, 0x00, // global.set 0
+ 0x0b,
+ ];
+ const checkBody = [
+ 0x00,
+ 0x23, 0x00, // global.get 0
+ 0xd1, // ref.is_null
+ 0x0b,
+ ];
+ const codeSec = [
+ 0x02,
+ ...uleb(poisonBody.length), ...poisonBody,
+ ...uleb(checkBody.length), ...checkBody,
+ ];
+ return new Uint8Array([
+ 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
+ ...section(1, typeSec),
+ ...section(3, funcSec),
+ ...section(6, globalSec),
+ ...section(7, exportSec),
+ ...section(10, codeSec),
+ ]);
+}
+
+// prefix sub-op flags label ht1 ht2
+const CANONICAL = [0xfb, 0x18, 0x03, 0x00, 0x6e, 0x00];
+const OVERLONG = [0xfb, 0x98,0x80,0x00, 0x03, 0x00, 0x6e, 0x00];
+
+const a = new WebAssembly.Instance(new WebAssembly.Module(buildModule(CANONICAL)));
+const b = new WebAssembly.Instance(new WebAssembly.Module(buildModule(OVERLONG)));
+
+a.exports.poison();
+b.exports.poison();
+
+const rA = a.exports.is_poisoned();
+const rB = b.exports.is_poisoned();
+
+if (rA !== rB)
+ throw new Error("LEB encoding of GC sub-opcode affects br_on_cast semantics: "
+ + "canonical=" + rA + " overlong=" + rB
+ + " — IPInt reads flags from PC at hardcoded offset");
diff --git a/Source/JavaScriptCore/llint/InPlaceInterpreter64.asm b/Source/JavaScriptCore/llint/InPlaceInterpreter64.asm
index 4833cb2b6f81..c572f822e86e 100644
--- a/Source/JavaScriptCore/llint/InPlaceInterpreter64.asm
+++ b/Source/JavaScriptCore/llint/InPlaceInterpreter64.asm
@@ -3585,9 +3585,7 @@ end)
ipintOp(_br_on_cast, macro()
validateOpcodeConfig(a1)
loadi IPInt::RefTestCastMetadata::toHeapType[MC], a1
- # fb 18 FLAGS
- loadb 2[PC], a2
- rshifti 1, a2 # bit 1 = null2
+ loadb IPInt::RefTestCastMetadata::allowNull[MC], a2
loadq [sp], a3
operationCall(macro() cCall3(_ipint_extern_ref_test) end)
@@ -3603,9 +3601,7 @@ end)
ipintOp(_br_on_cast_fail, macro()
validateOpcodeConfig(a1)
loadi IPInt::RefTestCastMetadata::toHeapType[MC], a1
- loadb 2[PC], a2
- # fb 19 FLAGS
- rshifti 1, a2 # bit 1 = null2
+ loadb IPInt::RefTestCastMetadata::allowNull[MC], a2
loadq [sp], a3
operationCall(macro() cCall3(_ipint_extern_ref_test) end)
diff --git a/Source/JavaScriptCore/wasm/WasmIPIntGenerator.cpp b/Source/JavaScriptCore/wasm/WasmIPIntGenerator.cpp
index fde5d1d88718..3cc1d04b57a4 100644
--- a/Source/JavaScriptCore/wasm/WasmIPIntGenerator.cpp
+++ b/Source/JavaScriptCore/wasm/WasmIPIntGenerator.cpp
@@ -1368,20 +1368,22 @@ WARN_UNUSED_RETURN PartialResult IPIntGenerator::addStructSet(ExpressionType, co
return { };
}
-WARN_UNUSED_RETURN PartialResult IPIntGenerator::addRefTest(ExpressionType, bool, int32_t heapType, bool, ExpressionType&)
+WARN_UNUSED_RETURN PartialResult IPIntGenerator::addRefTest(ExpressionType, bool allowNull, int32_t heapType, bool, ExpressionType&)
{
m_metadata->appendMetadata<IPInt::RefTestCastMetadata>({
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: OpenAI Codex Security - Amy Burnett
Key insight. A polymorphic call was linked using *callLinkInfo even after virtualForWithFunction made a host call that ran JS and could free it; only linking when the callee cell is non-null (and setting it for InternalFunction) closes the UAF.
Patch Details
diff --git a/JSTests/stress/operation-polymorphic-call-host-call-ic-reset.js b/JSTests/stress/operation-polymorphic-call-host-call-ic-reset.js
new file mode 100644
index 000000000000..91a6f510b69e
--- /dev/null
+++ b/JSTests/stress/operation-polymorphic-call-host-call-ic-reset.js
@@ -0,0 +1,33 @@
+//@ runDefault("--useConcurrentJIT=false", "--jitPolicyScale=0.1")
+var proto = {};
+
+function g1() { return 1; }
+function g2() { return 2; }
+function g3() { return 3; }
+
+proto.__defineGetter__("p", g1);
+
+var o = Object.create(proto);
+
+function getP(obj) {
+ return obj.p;
+}
+noInline(getP);
+
+for (var i = 0; i < 80; ++i)
+ getP(o);
+
+proto.__defineGetter__("p", g2);
+getP(o);
+proto.__defineGetter__("p", g3);
+getP(o);
+
+var evil = new Proxy(function() {}, {
+ apply: function(target, thisArg, args) {
+ delete proto.p;
+ return 0x42;
+ }
+});
+proto.__defineGetter__("p", evil);
+
+getP(o);
diff --git a/Source/JavaScriptCore/bytecode/RepatchInlines.h b/Source/JavaScriptCore/bytecode/RepatchInlines.h
index d48999e0fe74..71d3708ba599 100644
--- a/Source/JavaScriptCore/bytecode/RepatchInlines.h
+++ b/Source/JavaScriptCore/bytecode/RepatchInlines.h
@@ -237,7 +237,8 @@ ALWAYS_INLINE void* virtualForWithFunction(VM& vm, JSCell* owner, CallFrame* cal
JSValue calleeAsValue = calleeFrame->guaranteedJSValueCallee();
calleeAsFunctionCell = getJSFunction(calleeAsValue);
if (!calleeAsFunctionCell) [[unlikely]] {
- if (is<InternalFunction>(calleeAsValue)) {
+ if (auto* internalFunction = dynamicDowncast<InternalFunction>(calleeAsValue)) {
+ calleeAsFunctionCell = internalFunction;
CodePtr<JSEntryPtrTag> codePtr = vm.getCTIInternalFunctionTrampolineFor(kind);
ASSERT(!!codePtr);
return codePtr.taggedPtr();
diff --git a/Source/JavaScriptCore/jit/JITOperations.cpp b/Source/JavaScriptCore/jit/JITOperations.cpp
index 2f2475fded76..094443ecfe81 100644
--- a/Source/JavaScriptCore/jit/JITOperations.cpp
+++ b/Source/JavaScriptCore/jit/JITOperations.cpp
@@ -2526,7 +2526,9 @@ JSC_DEFINE_JIT_OPERATION(operationPolymorphicCall, UCPURegister, (CallFrame* cal
void* callTarget = virtualForWithFunction(vm, owner, calleeFrame, callLinkInfo, calleeAsFunctionCell);
if (scope.exception()) [[unlikely]]
OPERATION_RETURN(scope, std::bit_cast<UCPURegister>(vm.getCTIStub(CommonJITThunkID::ThrowExceptionFromCall).template retagged<JSEntryPtrTag>().code().taggedPtr()));
- linkPolymorphicCall(vm, owner, calleeFrame, *callLinkInfo, CallVariant(calleeAsFunctionCell));
+ // A null cell means virtualForWithFunction did a host call that ran JS and may have freed *callLinkInfo; don't link.
+ if (calleeAsFunctionCell) [[likely]]
+ linkPolymorphicCall(vm, owner, calleeFrame, *callLinkInfo, CallVariant(calleeAsFunctionCell));
// Keep owner alive explicitly. Now this function can be called from tail-call. This means that CallFrame for that owner already goes away, so we should keep it alive if we would like to use it.
ensureStillAliveHere(owner);
if (scope.exception()) [[unlikely]]
diff --git a/Source/JavaScriptCore/llint/LLIntSlowPaths.cpp b/Source/JavaScriptCore/llint/LLIntSlowPaths.cpp
index fd806e5d0835..b135cac3302f 100644
--- a/Source/JavaScriptCore/llint/LLIntSlowPaths.cpp
+++ b/Source/JavaScriptCore/llint/LLIntSlowPaths.cpp
@@ -653,7 +653,9 @@ extern "C" UGPRPair SYSV_ABI llint_polymorphic_call(CallFrame* calleeFrame, Call
void* callTarget = virtualForWithFunction(vm, owner, calleeFrame, callLinkInfo, calleeAsFunctionCell);
if (scope.exception()) [[unlikely]]
return encodeResult(callTarget, std::bit_cast<void*>(&vm));
- linkPolymorphicCall(vm, owner, calleeFrame, *callLinkInfo, CallVariant(calleeAsFunctionCell));
+ // A null cell means virtualForWithFunction did a host call that ran JS and may have freed *callLinkInfo; don't link.
+ if (calleeAsFunctionCell) [[likely]]
+ linkPolymorphicCall(vm, owner, calleeFrame, *callLinkInfo, CallVariant(calleeAsFunctionCell));
ensureStillAliveHere(owner);
if (scope.exception()) [[unlikely]]
return encodeResult(callTarget, std::bit_cast<void*>(&vm));
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: OpenAI Codex Security - Amy Burnett
Key insight. Arguments-elimination used liveAtTail, which omits values kept alive only by an exceptional exit to a catch entrypoint (a non-CFG successor), so it could clobber the recoveries of an arguments object still live via catch; unioning in terminal bytecode/OSR liveness closes the gap.
Patch Details
diff --git a/JSTests/stress/arguments-elimination-inlined-load-varargs-preserves-recoveries.js b/JSTests/stress/arguments-elimination-inlined-load-varargs-preserves-recoveries.js
new file mode 100644
index 000000000000..6a9e21ffb290
--- /dev/null
+++ b/JSTests/stress/arguments-elimination-inlined-load-varargs-preserves-recoveries.js
@@ -0,0 +1,84 @@
+//@ runDefault("--thresholdForJITAfterWarmUp=10", "--thresholdForFTLOptimizeAfterWarmUp=1000", "--useConcurrentJIT=false", "--validateFTLOSRExitLiveness=true")
+
+"use strict";
+
+function shouldBe(actual, expected)
+{
+ if (actual !== expected)
+ throw new Error("bad value: " + actual + ", expected: " + expected);
+}
+
+function five(values1, values2)
+{
+ let result = null;
+ for (let i = 0; i < 5; ++i) {
+ function arg() { "use strict"; return arguments; }
+ const a = arg.apply(undefined, values1);
+ const b = arg.apply(undefined, values2);
+ try {
+ (3881)(b);
+ } catch (error) {
+ a.toString();
+ result = a;
+ }
+ }
+ return result;
+}
+
+function eight(values1, values2)
+{
+ let result = null;
+ for (let i = 0; i < 5; ++i) {
+ function arg() { "use strict"; return arguments; }
+ const a = arg.apply(undefined, values1);
+ const b = arg.apply(undefined, values2);
+ try {
+ (3881)(b);
+ } catch (error) {
+ a.toString();
+ result = a;
+ }
+ }
+ return result;
+}
+
+function filled(length, value)
+{
+ const result = [];
+ for (let i = 0; i < length; ++i)
+ result.push(value);
+ return result;
+}
+
+const fiveMarker = { marker: "five" };
+const eightMarker = { marker: "eight" };
+const seedArray = [{ marker: "seed" }, 1, 2, 3, 4, 5];
+
+const firstFive = filled(5, fiveMarker);
+const overwriteFive = filled(30, fiveMarker);
+overwriteFive[22] = 9;
+
+const firstEight = filled(8, eightMarker);
+const overwriteEight = filled(30, eightMarker);
+overwriteEight[20] = 9;
+
+for (let i = 0; i < testLoopCount; ++i) {
+ five(firstFive, overwriteFive);
+ eight(firstEight, overwriteEight);
+}
+
+const seedValues = filled(30, seedArray);
+seedValues[20] = 9;
+for (let i = 0; i < testLoopCount; ++i)
+ eight(firstEight, seedValues);
+
+const recoveredEight = eight(firstEight, seedValues);
+shouldBe(recoveredEight.length, firstEight.length);
+for (let i = 0; i < firstEight.length; ++i)
+ shouldBe(recoveredEight[i], eightMarker);
+
+const recoveredFive = five(firstFive, overwriteFive);
+shouldBe(recoveredFive.length, firstFive.length);
+for (let i = 0; i < firstFive.length; ++i)
+ shouldBe(recoveredFive[i], fiveMarker);
+shouldBe(recoveredFive[5], undefined);
diff --git a/JSTests/stress/arguments-elimination-load-varargs-kills-promoted-recoveries.js b/JSTests/stress/arguments-elimination-load-varargs-kills-promoted-recoveries.js
new file mode 100644
index 000000000000..aa818cb6d313
--- /dev/null
+++ b/JSTests/stress/arguments-elimination-load-varargs-kills-promoted-recoveries.js
@@ -0,0 +1,88 @@
+//@ runDefault("--thresholdForJITAfterWarmUp=10", "--thresholdForFTLOptimizeAfterWarmUp=1000", "--useConcurrentJIT=false")
+
+"use strict";
+
+function shouldBe(actual, expected)
+{
+ if (actual !== expected)
+ throw new Error("bad value: " + actual + ", expected: " + expected);
+}
+noInline(shouldBe);
+
+function five(values1, values2)
+{
+ let result = null;
+ for (let i = 0; i < 5; ++i) {
+ function arg() { "use strict"; return arguments; }
+ const a = arg.apply(undefined, values1);
+ const b = arg.apply(undefined, values2);
+ try {
+ (3881)(b);
+ } catch (error) {
+ a.toString();
+ result = a;
+ }
+ }
+ return result;
+}
+noInline(five);
+
+function eight(values1, values2)
+{
+ let result = null;
+ for (let i = 0; i < 5; ++i) {
+ function arg() { "use strict"; return arguments; }
+ const a = arg.apply(undefined, values1);
+ const b = arg.apply(undefined, values2);
+ try {
+ (3881)(b);
+ } catch (error) {
+ a.toString();
+ result = a;
+ }
+ }
+ return result;
+}
+noInline(eight);
+
+function filled(length, value)
+{
+ const result = [];
+ for (let i = 0; i < length; ++i)
+ result.push(value);
+ return result;
+}
+noInline(filled);
+
+const fiveMarker = { marker: "five" };
+const eightMarker = { marker: "eight" };
+const seedArray = [{ marker: "seed" }, 1, 2, 3, 4, 5];
+
+const firstFive = filled(5, fiveMarker);
+const overwriteFive = filled(30, fiveMarker);
+overwriteFive[22] = 9;
+
+const firstEight = filled(8, eightMarker);
+const overwriteEight = filled(30, eightMarker);
+overwriteEight[20] = 9;
+
+for (let i = 0; i < testLoopCount; ++i) {
+ five(firstFive, overwriteFive);
+ eight(firstEight, overwriteEight);
+}
+
+const seedValues = filled(30, seedArray);
+seedValues[20] = 9;
+for (let i = 0; i < testLoopCount; ++i)
+ eight(firstEight, seedValues);
+
+const recoveredEight = eight(firstEight, seedValues);
+shouldBe(recoveredEight.length, firstEight.length);
+for (let i = 0; i < firstEight.length; ++i)
+ shouldBe(recoveredEight[i], eightMarker);
+
+const recoveredFive = five(firstFive, overwriteFive);
+shouldBe(recoveredFive.length, firstFive.length);
+for (let i = 0; i < firstFive.length; ++i)
+ shouldBe(recoveredFive[i], fiveMarker);
+shouldBe(recoveredFive[5], undefined);
diff --git a/Source/JavaScriptCore/dfg/DFGArgumentsEliminationPhase.cpp b/Source/JavaScriptCore/dfg/DFGArgumentsEliminationPhase.cpp
index b90a0b9649cf..52567cea948e 100644
--- a/Source/JavaScriptCore/dfg/DFGArgumentsEliminationPhase.cpp
+++ b/Source/JavaScriptCore/dfg/DFGArgumentsEliminationPhase.cpp
@@ -33,6 +33,7 @@
#include "DFGArgumentsUtilities.h"
#include <wtf/IndexMap.h>
#include "DFGClobberize.h"
+#include "DFGCombinedLiveness.h"
#include "DFGForAllKills.h"
#include "DFGGraph.h"
#include "DFGInsertionSet.h"
@@ -722,7 +723,16 @@ class ArgumentsEliminationPhase : public Phase {
}
if (clobberStack) {
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: Claudio Bozzato and Francesco Benvenuto of Cisco Talos, Josef Korbel (Citadelo)
Key insight. During backtracking with the first-non-BMP-character optimization, the input index was adjusted for a surrogate pair without re-validating input bounds, so a match’s end could fall before its start or past the subject. The fix re-checks bounds through checkInput() after the adjustment and asserts end >= start when building the matches array.
Overview
Impact:
Description: Processing maliciously crafted web content may lead to memory corruption
Researchers: Henock Habte
Key insight. A page-controlled ratio (repeatingDuration/simpleDuration) was cast to unsigned with static_cast, which wraps, and each skipped iteration dispatched its own event. The fix clamps the count with clampTo<unsigned>(), guards the decrement against underflow, and coalesces the per-iteration event loop into a single O(1) dispatch.
Patch Details
diff --git a/LayoutTests/svg/animations/smil-seek-huge-repeat-count-crash-expected.txt b/LayoutTests/svg/animations/smil-seek-huge-repeat-count-crash-expected.txt
new file mode 100644
index 000000000000..cd68e3612acf
--- /dev/null
+++ b/LayoutTests/svg/animations/smil-seek-huge-repeat-count-crash-expected.txt
@@ -0,0 +1,3 @@
+Passes if it does not crash.
+
+
diff --git a/LayoutTests/svg/animations/smil-seek-huge-repeat-count-crash.html b/LayoutTests/svg/animations/smil-seek-huge-repeat-count-crash.html
new file mode 100644
index 000000000000..c30fa119a3e8
--- /dev/null
+++ b/LayoutTests/svg/animations/smil-seek-huge-repeat-count-crash.html
@@ -0,0 +1,14 @@
+<body>
+ <p>Passes if it does not crash.</p>
+ <svg id="svg">
+ <rect width="100" height="100" fill="green">
+ <animate attributeName="x" from="0" to="10" dur="0.0001s" repeatCount="indefinite"/>
+ </rect>
+ </svg>
+ <script>
+ if (window.testRunner)
+ testRunner.dumpAsText();
+
+ svg.setCurrentTime(400000);
+ </script>
+</body>
diff --git a/Source/WebCore/svg/animation/SVGSMILElement.cpp b/Source/WebCore/svg/animation/SVGSMILElement.cpp
index 8bd88254df52..8a9d6c1670b1 100644
--- a/Source/WebCore/svg/animation/SVGSMILElement.cpp
+++ b/Source/WebCore/svg/animation/SVGSMILElement.cpp
@@ -1049,12 +1049,13 @@ float SVGSMILElement::calculateAnimationPercentAndRepeat(SMILTime elapsed, unsig
SMILTime activeTime = elapsed - m_intervalBegin;
SMILTime repeatingDuration = this->repeatingDuration();
+ // Clamp the page-controlled repeat count to prevent overflow.
if ((elapsed >= m_intervalEnd && !repeatingDuration.isIndefinite()) || activeTime > repeatingDuration) {
- repeat = static_cast<unsigned>(repeatingDuration.value() / simpleDuration.value());
- if (!fmod(repeatingDuration.value(), simpleDuration.value()))
+ repeat = clampTo<unsigned>(repeatingDuration.value() / simpleDuration.value());
+ if (repeat && !fmod(repeatingDuration.value(), simpleDuration.value()))
--repeat;
} else
- repeat = static_cast<unsigned>(activeTime.value() / simpleDuration.value());
+ repeat = clampTo<unsigned>(activeTime.value() / simpleDuration.value());
double percent;
if (elapsed >= m_intervalEnd || activeTime > repeatingDuration) {
@@ -1187,16 +1188,9 @@ bool SVGSMILElement::progress(SMILTime elapsed, SVGSMILElement& firstAnimation,
if (m_activeState == Inactive || m_activeState == Frozen)
smilEventSender().dispatchEventSoon(*this, eventNames().endEventEvent);
- if (repeat) {
- // We intentionally dispatch repeat - 1 events here because the first repeat
- // event (for the initial loop) is sent elsewhere during continuous animation run.
- // If repeat == 1, no events are dispatched here.
- for (unsigned i = 0; i < repeat - 1; ++i)
- smilEventSender().dispatchEventSoon(*this, eventNames().repeatEventEvent);
-
- if (m_activeState == Inactive)
- smilEventSender().dispatchEventSoon(*this, eventNames().repeatEventEvent);
- }
+ // Coalesce the skipped repeat iterations into a single event instead of one per interval.
+ if (repeat > 1 || (repeat && m_activeState == Inactive))
+ smilEventSender().dispatchEventSoon(*this, eventNames().repeatEventEvent);
}
m_nextProgressTime = calculateNextProgressTime(elapsed);
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: Niels Hofmans
Background
Advisory: Apple Security Advisory
| Attribute | Value |
|---|---|
| CVE | CVE-2026-65351 |
| Bugzilla | 321517 |
| Component | WebKit |
| Bug Class | LogicError |
| Severity | medium |
| Advisory | Apple Advisory |
Fix not public. The WebKit fix for this bug is embargoed or not yet disclosed. The bug ID is confirmed from the Apple advisory, but no public commit references it.
Root Cause Analysis
This issue was addressed through improved state management.
Overview
Impact:
Description: Visiting a website may lead to an app denial-of-service
Researchers: Heiko Kiesel of SEEMOO, TU Darmstadt
Key insight. Programmatic focus of another window via a named-target window.open was gated on page visibility but not on consuming a transient user activation, letting a page refocus its opener (a popunder) with no fresh gesture.
Patch Details
diff --git a/Source/WebCore/loader/FrameLoader.cpp b/Source/WebCore/loader/FrameLoader.cpp
index b8b82805eb5b..0191bc145bab 100644
--- a/Source/WebCore/loader/FrameLoader.cpp
+++ b/Source/WebCore/loader/FrameLoader.cpp
@@ -4872,7 +4872,8 @@ std::pair<RefPtr<Frame>, CreatedNewPage> createWindow(LocalFrame& openerFrame, F
if (!request.frameName().isEmpty() && !isBlankTargetFrameName(request.frameName())) {
if (RefPtr frame = openerFrame.loader().findFrameForNavigation(request.frameName(), openerFrame.protectedDocument().get())) {
if (!isSelfTargetFrameName(request.frameName())) {
- if (RefPtr page = frame->page(); page && isInVisibleAndActivePage(openerFrame))
+ RefPtr openerWindow = openerFrame.window();
+ if (RefPtr page = frame->page(); page && isInVisibleAndActivePage(openerFrame) && openerWindow && openerWindow->consumeTransientActivation())
page->chrome().focus();
}
frame->updateOpener(openerFrame);
diff --git a/Tools/TestWebKitAPI/Tests/WebKitCocoa/VerifyUserGestureFromUIProcess.mm b/Tools/TestWebKitAPI/Tests/WebKitCocoa/VerifyUserGestureFromUIProcess.mm
index bfdac0f91dfd..70993eff08d7 100644
--- a/Tools/TestWebKitAPI/Tests/WebKitCocoa/VerifyUserGestureFromUIProcess.mm
+++ b/Tools/TestWebKitAPI/Tests/WebKitCocoa/VerifyUserGestureFromUIProcess.mm
@@ -269,6 +269,70 @@ HTTPServer server({
// during popup creation.
EXPECT_FALSE(focusCalled);
}
+
+TEST(VerifyUserGesture, PopunderPreventedViaDualEventListeners)
+{
+ auto openerHTML = "<script>"
+ "window.name = 'opener';"
+ "let popup;"
+ "let opened = false;"
+ "addEventListener('pointerdown', () => {"
+ " if (!opened) {"
+ " opened = true;"
+ " popup = window.open('https://domain2.com/popup');"
+ " }"
+ "});"
+ "addEventListener('click', () => {"
+ " if (popup && popup.refocusOpener) popup.refocusOpener();"
+ "});"
+ "</script>"_s;
+ auto popupHTML = "<script>"
+ "function refocusOpener() { window.open('', 'opener'); }"
+ "</script>"_s;
+ HTTPServer server({
+ { "/opener"_s, { openerHTML } },
+ { "/popup"_s, { popupHTML } }
+ }, HTTPServer::Protocol::HttpsProxy);
+
+ RetainPtr configuration = server.httpsProxyConfiguration();
+ RetainPtr navigationDelegate = adoptNS([TestNavigationDelegate new]);
+ [navigationDelegate allowAnyTLSCertificate];
+ RetainPtr uiDelegate = adoptNS([TestUIDelegate new]);
+ RetainPtr openerWebView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]);
+ [openerWebView setNavigationDelegate:navigationDelegate.get()];
+ [openerWebView setUIDelegate:uiDelegate.get()];
+ [openerWebView configuration].preferences.javaScriptCanOpenWindowsAutomatically = YES;
+
+ __block RetainPtr<TestWKWebView> popupWebView;
+ __block RetainPtr<TestNavigationDelegate> popupNavigationDelegate;
+ uiDelegate.get().createWebViewWithConfiguration = ^(WKWebViewConfiguration *configuration, WKNavigationAction *navigationAction, WKWindowFeatures *) {
+ [navigationAction._userInitiatedAction consume];
+ popupNavigationDelegate = adoptNS([TestNavigationDelegate new]);
+ [popupNavigationDelegate allowAnyTLSCertificate];
+ popupWebView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration]);
+ [popupWebView setNavigationDelegate:popupNavigationDelegate.get()];
+ return popupWebView.get();
+ };
+
+ __block bool focusCalled = false;
+ uiDelegate.get().focusWebView = ^(WKWebView *) {
+ focusCalled = true;
+ };
+
+ [openerWebView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://domain1.com/opener"]]];
+ [navigationDelegate waitForDidFinishNavigation];
+
+ [openerWebView mouseDownAtPoint:CGPointMake(50, 50) simulatePressure:NO];
+ [openerWebView mouseUpAtPoint:CGPointMake(50, 50)];
+ [openerWebView waitForPendingMouseEvents];
+ while (!popupWebView)
+ Util::spinRunLoop();
+ [popupNavigationDelegate waitForDidFinishNavigation];
+
+ Util::spinRunLoop(10);
+
+ EXPECT_FALSE(focusCalled);
+}
#endif
}
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: An app may be able to read files outside of its sandbox
Researchers: Brian Carpenter
Key insight. A compatibility exemption written for third-party apps (which legitimately load local files without a sandbox extension) was scoped too broadly and also covered MobileSafari. Adding
!WTF::IOSApplication::isMobileSafari()narrows the exemption to the apps that need it and forces Safari back onto the normal sandbox-extension path.
Patch Details
diff --git a/Source/WebKit/NetworkProcess/NetworkResourceLoader.cpp b/Source/WebKit/NetworkProcess/NetworkResourceLoader.cpp
index 91809cdd24f4..ccfe30b53983 100644
--- a/Source/WebKit/NetworkProcess/NetworkResourceLoader.cpp
+++ b/Source/WebKit/NetworkProcess/NetworkResourceLoader.cpp
@@ -103,6 +103,7 @@
#if PLATFORM(COCOA)
#include "PathsBlockedForSandboxExtensions.h"
+#include <wtf/cocoa/RuntimeApplicationChecksCocoa.h>
#endif
#define LOADER_RELEASE_LOG_WITH_THIS(thisPtr, fmt, ...) RELEASE_LOG(Network, "%p - [pageProxyID=%" PRIu64 ", webPageID=%" PRIu64 ", frameID=%" PRIu64 ", resourceID=%" PRIu64 ", isMainResource=%d, destination=%u, isSynchronous=%d] NetworkResourceLoader::" fmt, WTF::getPtr(thisPtr), thisPtr->webPageProxyID().toUInt64(), thisPtr->pageID().toUInt64(), thisPtr->frameID().toUInt64(), thisPtr->coreIdentifier().toUInt64(), thisPtr->isMainResource(), static_cast<unsigned>(thisPtr->m_parameters.options.destination), thisPtr->isSynchronous(), ##__VA_ARGS__)
@@ -396,13 +397,18 @@ bool NetworkResourceLoader::shouldSendResourceLoadMessages() const
bool NetworkResourceLoader::isLocalFileLoadAllowed(const URL& url)
{
#if PLATFORM(IOS_FAMILY)
- // Some applications are relying on using the fetch JS API to load local files they have created in their temp directory.
+ // Some 3rd party apps are relying on using the fetch JS API or -[WKWebView loadHTMLString:baseURL:] to load local files in their temp directory.
// In this case, the WebContent process will not provide the Networking process with a sandbox extension to that file, since it does not have access.
// This is because the load is not initiated from the UI process which would provide an extension, but from JS in the WebContent process.
// To continue supporting this undocumented feature, we should allow local file loads from that location.
+ // FIXME: rdar://177160334
+ // The method -[WKWebView loadHTMLString:baseURL:] can be used to load local files by referring to links relative to the base URL in the HTML string.
+ // When the app is using -[WKWebView loadHTMLString:baseURL:] to load files in the temp directory, we should create a sandbox extension for the base URL.
+ // This can be done in WebPageProxy::loadDataWithNavigationShared. However, this is a larger change, so for now we rely on this exemption.
+
String directory = connectionToWebProcess().networkProcess().containerTemporaryDirectory();
- if (!directory.isEmpty() && FileSystem::isAncestor(directory, FileSystem::realPath(url.fileSystemPath()))) {
+ if (!WTF::IOSApplication::isMobileSafari() && !directory.isEmpty() && FileSystem::isAncestor(directory, FileSystem::realPath(url.fileSystemPath()))) {
RELEASE_LOG(Network, "shouldAllowLocalFileLoad: allowing loads from the temp directory");
return true;
}
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Websites may know if the user has visited a given link
Researchers: Kwak Kiyong, Song Nuri
Key insight. LCP renderTime was exposed at full resolution instead of the spec-mandated 4ms coarsening, turning paint timing into a side channel for visited-link/cache inference.
Patch Details
diff --git a/Source/WebCore/page/LargestContentfulPaintData.cpp b/Source/WebCore/page/LargestContentfulPaintData.cpp
index 772b835081bc..45d59aa9db63 100644
--- a/Source/WebCore/page/LargestContentfulPaintData.cpp
+++ b/Source/WebCore/page/LargestContentfulPaintData.cpp
@@ -198,13 +198,21 @@ void LargestContentfulPaintData::potentiallyAddLargestContentfulPaintEntry(Eleme
pendingEntry->setURLString(image->url().string());
auto loadTimestamp = protect(window->performance())->relativeTimeFromTimeOriginInReducedResolution(loadTime);
pendingEntry->setLoadTime(loadTimestamp);
- }
+
+ // FIXME: Adopt ReducedResolutionSeconds: webkit.org/b/316824.
+ auto reduceResolution = [](Seconds value, Seconds resolution) {
+ return Seconds(std::floor(value.value() / resolution.value()) * resolution.value());
+ };
+
+ // https://w3c.github.io/largest-contentful-paint/#sec-report-largest-contentful-paint coarsens renderTime to 4ms for images.
+ static constexpr auto renderTimeSecondsResolution = 4_ms;
+ pendingEntry->setRenderTime(reduceResolution(Seconds::fromMilliseconds(paintTimestamp), renderTimeSecondsResolution).milliseconds());
+ } else
+ pendingEntry->setRenderTime(paintTimestamp);
if (element.hasID())
pendingEntry->setID(element.getIdAttribute().string());
- pendingEntry->setRenderTime(paintTimestamp);
-
LOG_WITH_STREAM(LargestContentfulPaint, stream << " making new entry for " << element << " image " << (image ? image->url().string() : emptyString()) << " id " << pendingEntry->id() <<
": entry size " << pendingEntry->size() << ", loadTime " << pendingEntry->loadTime() << ", renderTime " << pendingEntry->renderTime());
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: OGINOME Tomohito, an anonymous researcher
Key insight. A single static, mutable CGContext was written on the assumption of single-threaded access, but OffscreenCanvas made stroke queries concurrent. The fix serializes access with a function-local static Lock — the shared resource needed synchronization the moment worker threads could reach it.
Patch Details
diff --git a/LayoutTests/fast/canvas/offscreen-worker-unsafe-isPointInStroke-call-expected.txt b/LayoutTests/fast/canvas/offscreen-worker-unsafe-isPointInStroke-call-expected.txt
new file mode 100644
index 000000000000..4fd0bd3beec1
--- /dev/null
+++ b/LayoutTests/fast/canvas/offscreen-worker-unsafe-isPointInStroke-call-expected.txt
@@ -0,0 +1 @@
+PASS if this test does not crash.
diff --git a/LayoutTests/fast/canvas/offscreen-worker-unsafe-isPointInStroke-call.html b/LayoutTests/fast/canvas/offscreen-worker-unsafe-isPointInStroke-call.html
new file mode 100644
index 000000000000..86fa74bd492a
--- /dev/null
+++ b/LayoutTests/fast/canvas/offscreen-worker-unsafe-isPointInStroke-call.html
@@ -0,0 +1,51 @@
+<body>
+ <p>PASS if this test does not crash.</p>
+ <script>
+ window.testRunner?.waitUntilDone();
+ window.testRunner?.dumpAsText();
+
+ const tester = `self.onmessage = event => {
+ const ctx = event.data.getContext("2d");
+ for (let i = 0; i < 500; i++) {
+ ctx.setLineDash([5 + i % 20, 3 + i % 12]); // forces CG dash alloc/free
+ ctx.beginPath();
+ for (let j = 0; j < 25; j++) {
+ let a = j / 25 * Math.PI * 2 + i * .1;
+ let r = 30 + 20 * Math.sin(j * .7 + i * .3);
+ let x = 64 + r * Math.cos(a);
+ let y = 64 + r * Math.sin(a);
+ if (!j)
+ ctx.moveTo(x, y);
+ else
+ ctx.bezierCurveTo(
+ x + 12 * Math.sin(i + j), y + 12 * Math.cos(i + j),
+ x - 8 * Math.cos(i * j), y - 8 * Math.sin(i * j),
+ x, y
+ );
+ }
+ ctx.closePath();
+ ctx.isPointInStroke(64, 64);
+ }
+ self.postMessage({ type: 'done' }); // Send finished signal
+ };`;
+
+ const totalWorkers = 4;
+ let doneWorkers = 0;
+
+ for (let i = 0; i < totalWorkers; i++) {
+ const canvas = document.createElement("canvas");
+ canvas.width = 128;
+ canvas.height = 128;
+ const offscreen = canvas.transferControlToOffscreen();
+ const worker = new Worker(URL.createObjectURL(new Blob([tester])));
+ worker.postMessage(offscreen, [offscreen]);
+
+ worker.onmessage = event => {
+ if (event.data.type === 'done') {
+ if (++doneWorkers == totalWorkers)
+ window.testRunner?.notifyDone();
+ }
+ };
+ }
+ </script>
+</body>
diff --git a/Source/WebCore/platform/graphics/cg/PathCG.cpp b/Source/WebCore/platform/graphics/cg/PathCG.cpp
index 1a72e5045c38..32ae939bd590 100644
--- a/Source/WebCore/platform/graphics/cg/PathCG.cpp
+++ b/Source/WebCore/platform/graphics/cg/PathCG.cpp
@@ -606,6 +606,9 @@ bool PathCG::strokeContains(const FloatPoint& point, NOESCAPE const Function<voi
{
ASSERT(strokeStyleApplier);
+ static Lock scratchContextLock;
+ Locker locker { scratchContextLock };
+
CGContextRef context = scratchContext();
CGContextSaveGState(context);
@@ -641,6 +644,9 @@ FloatRect PathCG::boundingRect() const
FloatRect PathCG::strokeBoundingRect(NOESCAPE const Function<void(GraphicsContext&)>& strokeStyleApplier) const
{
+ static Lock scratchContextLock;
+ Locker locker { scratchContextLock };
+
CGContextRef context = scratchContext();
CGContextSaveGState(context);
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: Shaheen Fazim
Background
Advisory: Apple Security Advisory
| Attribute | Value |
|---|---|
| CVE | CVE-2026-64719 |
| Bugzilla | 319404 |
| Component | WebKit |
| Bug Class | OOB |
| Severity | medium |
| Advisory | Apple Advisory |
Fix not public. The WebKit fix for this bug is embargoed or not yet disclosed. The bug ID is confirmed from the Apple advisory, but no public commit references it.
Root Cause Analysis
An out-of-bounds access issue was addressed with improved bounds checking.
Overview
Impact:
Description: Maliciously crafted web content may violate iframe sandboxing policy
Researchers: an anonymous researcher
Key insight. The bug was in origin reasoning, not memory: the inherit-from-owner case (srcdoc/about:blank) was lumped with the registrable-domain match and given the top-level url as its site-for-cookies. The fix instead inherits the parent frame’s site-for-cookies, so a srcdoc nested inside a cross-origin iframe is correctly cross-site to the top origin.
Patch Details
diff --git a/LayoutTests/http/tests/cookies/same-site/fetch-in-srcdoc-iframe-inside-cross-origin-iframe-expected.txt b/LayoutTests/http/tests/cookies/same-site/fetch-in-srcdoc-iframe-inside-cross-origin-iframe-expected.txt
new file mode 100644
index 000000000000..2cb25ce2f4a6
--- /dev/null
+++ b/LayoutTests/http/tests/cookies/same-site/fetch-in-srcdoc-iframe-inside-cross-origin-iframe-expected.txt
@@ -0,0 +1,13 @@
+Tests that SameSite=Strict and SameSite=Lax cookies for 127.0.0.1 are not sent with a fetch() initiated from a srcdoc iframe nested inside a cross-origin iframe. The srcdoc inherits its parent's origin, so a request from the srcdoc back to the top-level origin is cross-site and must not receive SameSite-restricted cookies.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+Cookies sent with cross-site fetch initiated from srcdoc iframe inside cross-origin iframe:
+PASS Do not have cookie "strict".
+PASS Do not have cookie "lax".
+PASS Has cookie "implicit-default" with value 9.
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/http/tests/cookies/same-site/fetch-in-srcdoc-iframe-inside-cross-origin-iframe.html b/LayoutTests/http/tests/cookies/same-site/fetch-in-srcdoc-iframe-inside-cross-origin-iframe.html
new file mode 100644
index 000000000000..936fde8bb3e1
--- /dev/null
+++ b/LayoutTests/http/tests/cookies/same-site/fetch-in-srcdoc-iframe-inside-cross-origin-iframe.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src="/js-test-resources/js-test.js"></script>
+<script src="../resources/cookie-utilities.js"></script>
+</head>
+<body>
+<script>
+window.jsTestIsAsync = true;
+
+description("Tests that SameSite=Strict and SameSite=Lax cookies for 127.0.0.1 are not sent with a fetch() initiated from a srcdoc iframe nested inside a cross-origin iframe. The srcdoc inherits its parent's origin, so a request from the srcdoc back to the top-level origin is cross-site and must not receive SameSite-restricted cookies.");
+
+const kCookieValue = "9";
+
+function checkCookies(cookies)
+{
+ debug("Cookies sent with cross-site fetch initiated from srcdoc iframe inside cross-origin iframe:");
+ if (cookies.error) {
+ testFailed(`fetch from srcdoc failed: ${cookies.error}`);
+ return;
+ }
+ if (cookies.strict === undefined)
+ testPassed('Do not have cookie "strict".');
+ else
+ testFailed(`Should not have cookie "strict". But do with value ${cookies.strict}.`);
+
+ if (cookies.lax === undefined)
+ testPassed('Do not have cookie "lax".');
+ else
+ testFailed(`Should not have cookie "lax". But do with value ${cookies.lax}.`);
+
+ if (cookies["implicit-default"] === kCookieValue)
+ testPassed(`Has cookie "implicit-default" with value ${kCookieValue}.`);
+ else
+ testFailed(`Should have cookie "implicit-default" with value ${kCookieValue}. Got ${cookies["implicit-default"]}.`);
+}
+
+async function runTest()
+{
+ await resetCookies();
+ await setCookie("strict", kCookieValue, {"SameSite": "Strict", "Max-Age": 100, "path": "/"});
+ await setCookie("lax", kCookieValue, {"SameSite": "Lax", "Max-Age": 100, "path": "/"});
+ await setCookie("implicit-default", kCookieValue, {"SameSite": null, "Max-Age": 100, "path": "/"});
+
+ let cookiesPromise = new Promise((resolve) => {
+ window.addEventListener("message", (event) => {
+ if (event.data && event.data.type === "cookies-from-srcdoc")
+ resolve(event.data.cookies);
+ }, {once: true});
+ });
+
+ let attackerIframe = document.createElement("iframe");
+ attackerIframe.src = "http://localhost:8000/cookies/same-site/resources/srcdoc-creator-inside-cross-origin-iframe.html";
+ document.body.appendChild(attackerIframe);
+
+ let cookies = await cookiesPromise;
+ checkCookies(cookies);
+
+ document.body.removeChild(attackerIframe);
+
+ await resetCookies();
+ finishJSTest();
+}
+runTest();
+</script>
+</body>
+</html>
diff --git a/LayoutTests/http/tests/cookies/same-site/img-from-srcdoc-iframe-inside-cross-origin-iframe-expected.txt b/LayoutTests/http/tests/cookies/same-site/img-from-srcdoc-iframe-inside-cross-origin-iframe-expected.txt
new file mode 100644
index 000000000000..b1683955995f
--- /dev/null
+++ b/LayoutTests/http/tests/cookies/same-site/img-from-srcdoc-iframe-inside-cross-origin-iframe-expected.txt
@@ -0,0 +1,13 @@
+Tests that SameSite=Strict and SameSite=Lax cookies for 127.0.0.1 are not sent with an <img> sub-resource request initiated from a srcdoc iframe nested inside a cross-origin iframe. The srcdoc inherits its parent's origin, so the image request back to the top-level origin is cross-site and must not receive SameSite-restricted cookies.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+Cookies sent with cross-site <img> request initiated from srcdoc iframe inside cross-origin iframe:
+PASS Do not have cookie "strict".
+PASS Do not have cookie "lax".
+PASS Has cookie "implicit-default" with value 9.
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/http/tests/cookies/same-site/img-from-srcdoc-iframe-inside-cross-origin-iframe.html b/LayoutTests/http/tests/cookies/same-site/img-from-srcdoc-iframe-inside-cross-origin-iframe.html
new file mode 100644
index 000000000000..16e76c2c5593
--- /dev/null
+++ b/LayoutTests/http/tests/cookies/same-site/img-from-srcdoc-iframe-inside-cross-origin-iframe.html
@@ -0,0 +1,97 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src="/js-test-resources/js-test.js"></script>
+<script src="../resources/cookie-utilities.js"></script>
+</head>
+<body>
+<script>
+window.jsTestIsAsync = true;
+
+description("Tests that SameSite=Strict and SameSite=Lax cookies for 127.0.0.1 are not sent with an <img> sub-resource request initiated from a srcdoc iframe nested inside a cross-origin iframe. The srcdoc inherits its parent's origin, so the image request back to the top-level origin is cross-site and must not receive SameSite-restricted cookies.");
+
+const kCookieValue = "9";
+const kRecorderBase = "/cookies/same-site/resources/record-image-cookies.py";
+const kToken = "srcdoc-img-test-" + Date.now();
+
+async function fetchRecordedCookies()
+{
+ let response = await fetch(`${kRecorderBase}?mode=read&token=${encodeURIComponent(kToken)}`, {credentials: "same-origin"});
+ return response.json();
+}
+
+async function resetRecordedCookies()
+{
+ await fetch(`${kRecorderBase}?mode=reset&token=${encodeURIComponent(kToken)}`, {credentials: "same-origin"});
+}
+
+function checkCookies(cookies)
+{
+ debug("Cookies sent with cross-site <img> request initiated from srcdoc iframe inside cross-origin iframe:");
+ if (cookies.strict === undefined)
+ testPassed('Do not have cookie "strict".');
+ else
+ testFailed(`Should not have cookie "strict". But do with value ${cookies.strict}.`);
+
+ if (cookies.lax === undefined)
+ testPassed('Do not have cookie "lax".');
+ else
+ testFailed(`Should not have cookie "lax". But do with value ${cookies.lax}.`);
+
+ // Positive control: WebKit treats a cookie with no SameSite attribute as
+ // SameSite=None (see coreSameSitePolicy() in CookieCocoa.mm), so it must
+ // still be sent on a cross-site image request that includes credentials.
+ // Without this assertion, the negative checks above could pass simply
+ // because cookies aren't being delivered at all.
+ if (cookies["implicit-default"] === kCookieValue)
+ testPassed(`Has cookie "implicit-default" with value ${kCookieValue}.`);
+ else
+ testFailed(`Should have cookie "implicit-default" with value ${kCookieValue}. Got ${cookies["implicit-default"]}.`);
+}
+
+async function runTest()
+{
+ await resetCookies();
+ await resetRecordedCookies();
+ await setCookie("strict", kCookieValue, {"SameSite": "Strict", "Max-Age": 100, "path": "/"});
+ await setCookie("lax", kCookieValue, {"SameSite": "Lax", "Max-Age": 100, "path": "/"});
+ await setCookie("implicit-default", kCookieValue, {"SameSite": null, "Max-Age": 100, "path": "/"});
+
+ let imgLoadedPromise = new Promise((resolve, reject) => {
+ window.addEventListener("message", (event) => {
+ if (!event.data)
+ return;
+ if (event.data.type === "srcdoc-img-loaded")
+ resolve();
+ else if (event.data.type === "srcdoc-img-error")
+ reject(new Error("srcdoc <img> failed to load"));
+ }, {once: false});
+ });
+
+ let attackerIframe = document.createElement("iframe");
+ attackerIframe.src = "http://localhost:8000/cookies/same-site/resources/srcdoc-creator-img-inside-cross-origin-iframe.html?token=" + encodeURIComponent(kToken);
+ document.body.appendChild(attackerIframe);
+
+ try {
+ await imgLoadedPromise;
+ } catch (e) {
+ testFailed(String(e));
+ document.body.removeChild(attackerIframe);
+ await resetCookies();
+ finishJSTest();
+ return;
+ }
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Visiting a website that frames malicious content may lead to UI spoofing
Researchers: Kagami Rosylight of Mozilla
Key insight. Fullscreen Keyboard Lock was on by default, letting framed/hostile content capture the very keys (Escape/system chords) a user needs to escape a spoofed fullscreen; disabling it by default removes that trapping primitive.
Patch Details
diff --git a/Source/WTF/Scripts/Preferences/UnifiedWebPreferences.yaml b/Source/WTF/Scripts/Preferences/UnifiedWebPreferences.yaml
index f10613bab755..a0e2bf9ec94c 100644
--- a/Source/WTF/Scripts/Preferences/UnifiedWebPreferences.yaml
+++ b/Source/WTF/Scripts/Preferences/UnifiedWebPreferences.yaml
@@ -3552,18 +3552,18 @@ FullScreenEnabled:
FullScreenKeyboardLock:
type: bool
- status: stable
+ status: unstable
category: dom
humanReadableName: "Fullscreen API based Keyboard Lock"
humanReadableDescription: "Fullscreen API based Keyboard Lock"
condition: ENABLE(FULLSCREEN_API)
defaultValue:
WebKitLegacy:
- default: true
+ default: false
WebKit:
- default: true
+ default: false
WebCore:
- default: true
+ default: false
FullscreenRequirementForScreenOrientationLockingEnabled:
type: bool
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: Milad Nasr and Nicholas Carlini with Claude, Anthropic
Key insight. ScopedArgumentsTable kept a separate m_length that could drift from the m_arguments buffer size, so indexing by length could exceed the allocation; deriving length from the buffer and growing the parallel arrays together (checked) removes the desync.
Patch Details
diff --git a/Source/JavaScriptCore/runtime/CachedTypes.cpp b/Source/JavaScriptCore/runtime/CachedTypes.cpp
index 252090d11c10..7c6bdf8922d0 100644
--- a/Source/JavaScriptCore/runtime/CachedTypes.cpp
+++ b/Source/JavaScriptCore/runtime/CachedTypes.cpp
@@ -1202,15 +1202,15 @@ class CachedScopedArgumentsTable : public CachedObject<ScopedArgumentsTable> {
public:
void encode(Encoder& encoder, const ScopedArgumentsTable& scopedArgumentsTable)
{
- m_length = scopedArgumentsTable.m_length;
- m_arguments.encode(encoder, scopedArgumentsTable.m_arguments.get(), m_length);
+ m_length = scopedArgumentsTable.m_arguments.size();
+ m_arguments.encode(encoder, scopedArgumentsTable.m_arguments.span().data(), m_length);
}
ScopedArgumentsTable* decode(Decoder& decoder) const
{
ScopedArgumentsTable* scopedArgumentsTable = ScopedArgumentsTable::tryCreate(decoder.vm(), m_length);
RELEASE_ASSERT(scopedArgumentsTable); // We crash here. This is unlikely to continue execution if we hit this condition when decoding UnlinkedCodeBlock.
- m_arguments.decode(decoder, scopedArgumentsTable->m_arguments.get(), m_length);
+ m_arguments.decode(decoder, scopedArgumentsTable->m_arguments.mutableSpan().data(), m_length);
return scopedArgumentsTable;
}
diff --git a/Source/JavaScriptCore/runtime/ScopedArgumentsTable.cpp b/Source/JavaScriptCore/runtime/ScopedArgumentsTable.cpp
index 0dccadb737e8..24ff2effa9e2 100644
--- a/Source/JavaScriptCore/runtime/ScopedArgumentsTable.cpp
+++ b/Source/JavaScriptCore/runtime/ScopedArgumentsTable.cpp
@@ -38,7 +38,6 @@ const ClassInfo ScopedArgumentsTable::s_info = { "ScopedArgumentsTable"_s, nullp
ScopedArgumentsTable::ScopedArgumentsTable(VM& vm)
: Base(vm, vm.scopedArgumentsTableStructure.get())
- , m_length(0)
, m_locked(false)
{
}
@@ -66,21 +65,20 @@ ScopedArgumentsTable* ScopedArgumentsTable::tryCreate(VM& vm, uint32_t length)
ScopedArgumentsTable* result = new (NotNull, buffer) ScopedArgumentsTable(vm);
result->finishCreation(vm);
- result->m_length = length;
- result->m_arguments = ArgumentsPtr::tryCreate(length);
- if (!result->m_arguments) [[unlikely]]
+ if (!result->m_arguments.tryGrow(length)) [[unlikely]]
return nullptr;
- result->m_watchpointSets.fill(nullptr, length);
+ if (!result->m_watchpointSets.tryGrow(length)) [[unlikely]]
+ return nullptr;
+ std::ranges::fill(result->m_watchpointSets.mutableSpan(), nullptr);
return result;
}
ScopedArgumentsTable* ScopedArgumentsTable::tryClone(VM& vm)
{
- ScopedArgumentsTable* result = tryCreate(vm, m_length);
+ ScopedArgumentsTable* result = tryCreate(vm, m_arguments.size());
if (!result) [[unlikely]]
return nullptr;
- for (unsigned i = m_length; i--;)
- result->at(i) = this->at(i);
+ result->m_arguments = m_arguments;
result->m_watchpointSets = this->m_watchpointSets;
return result;
}
@@ -88,23 +86,21 @@ ScopedArgumentsTable* ScopedArgumentsTable::tryClone(VM& vm)
ScopedArgumentsTable* ScopedArgumentsTable::trySetLength(VM& vm, uint32_t newLength)
{
if (!m_locked) [[likely]] {
- ArgumentsPtr newArguments = ArgumentsPtr::tryCreate(newLength, newLength);
- if (!newArguments) [[unlikely]]
+ size_t oldSize = m_watchpointSets.size();
+ if (!m_arguments.tryGrow(newLength))
+ return nullptr;
+ if (!m_watchpointSets.tryGrow(newLength))
return nullptr;
- for (unsigned i = std::min(m_length, newLength); i--;)
- newArguments.at(i) = this->at(i);
- m_length = newLength;
- m_arguments = WTF::move(newArguments);
- m_watchpointSets.resize(newLength);
+ if (newLength > oldSize)
+ std::ranges::fill(m_watchpointSets.mutableSpan().subspan(oldSize), nullptr);
return this;
}
-
+
ScopedArgumentsTable* result = tryCreate(vm, newLength);
if (!result) [[unlikely]]
return nullptr;
- m_watchpointSets.resize(newLength);
- for (unsigned i = std::min(m_length, newLength); i--;) {
- result->at(i) = this->at(i);
+ for (unsigned i = std::min<uint32_t>(m_arguments.size(), newLength); i--;) {
+ result->m_arguments[i] = this->m_arguments[i];
result->m_watchpointSets[i] = this->m_watchpointSets[i];
}
return result;
@@ -121,7 +117,7 @@ ScopedArgumentsTable* ScopedArgumentsTable::trySet(VM& vm, uint32_t i, ScopeOffs
return nullptr;
} else
result = this;
- result->at(i) = value;
+ result->m_arguments[i] = value;
return result;
}
diff --git a/Source/JavaScriptCore/runtime/ScopedArgumentsTable.h b/Source/JavaScriptCore/runtime/ScopedArgumentsTable.h
index fe536388831e..261ce1bdfc3a 100644
--- a/Source/JavaScriptCore/runtime/ScopedArgumentsTable.h
+++ b/Source/JavaScriptCore/runtime/ScopedArgumentsTable.h
@@ -29,7 +29,7 @@
#include <JavaScriptCore/ScopeOffset.h>
#include <JavaScriptCore/VM.h>
#include <wtf/Assertions.h>
-#include <wtf/CagedUniquePtr.h>
+#include <wtf/Vector.h>
WTF_ALLOW_UNSAFE_BUFFER_USAGE_BEGIN
@@ -67,10 +67,10 @@ public:
static void destroy(JSCell*);
- uint32_t length() const { return m_length; }
+ uint32_t length() const { return m_arguments.size(); }
ScopedArgumentsTable* trySetLength(VM&, uint32_t newLength);
- ScopeOffset get(uint32_t i) const { return at(i); }
+ ScopeOffset get(uint32_t i) const { return m_arguments.at(i); }
WatchpointSet* getWatchpointSet(uint32_t i) const { return m_watchpointSets.at(i); }
void lock()
@@ -86,24 +86,17 @@ public:
static Structure* createStructure(VM&, JSGlobalObject*, JSValue prototype);
- static constexpr ptrdiff_t offsetOfLength() { return OBJECT_OFFSETOF(ScopedArgumentsTable, m_length); }
- static constexpr ptrdiff_t offsetOfArguments() { return OBJECT_OFFSETOF(ScopedArgumentsTable, m_arguments); }
+ using ArgumentScopeBufferType = Vector<ScopeOffset>;
- typedef CagedUniquePtr<Gigacage::Primitive, ScopeOffset> ArgumentsPtr;
+ static constexpr ptrdiff_t offsetOfLength() { return OBJECT_OFFSETOF(ScopedArgumentsTable, m_arguments) + ArgumentScopeBufferType::sizeMemoryOffset(); }
+ static constexpr ptrdiff_t offsetOfArguments() { return OBJECT_OFFSETOF(ScopedArgumentsTable, m_arguments) + ArgumentScopeBufferType::bufferMemoryOffset(); }
private:
ScopedArgumentsTable* tryClone(VM&);
-
- ScopeOffset& at(uint32_t i) const
- {
- ASSERT_WITH_SECURITY_IMPLICATION(i < m_length);
- return m_arguments.get()[i];
- }
- uint32_t m_length;
- bool m_locked; // Being locked means that there are multiple references to this object and none of them expect to see the others' modifications. This means that modifications need to make a copy first.
- ArgumentsPtr m_arguments;
+ ArgumentScopeBufferType m_arguments;
Vector<WatchpointSet*> m_watchpointSets;
+ bool m_locked; // Being locked means that there are multiple references to this object and none of them expect to see the others' modifications. This means that modifications need to make a copy first.
};
} // namespace JSC
diff --git a/Source/WTF/wtf/Vector.h b/Source/WTF/wtf/Vector.h
index 31240327b4bb..806004a5a1de 100644
--- a/Source/WTF/wtf/Vector.h
+++ b/Source/WTF/wtf/Vector.h
@@ -765,6 +765,7 @@ public:
[[nodiscard]] size_t size() const { return m_size; }
[[nodiscard]] size_t sizeInBytes() const { return static_cast<size_t>(m_size) * sizeof(T); }
static constexpr ptrdiff_t sizeMemoryOffset() { return OBJECT_OFFSETOF(Vector, m_size); }
+ static constexpr ptrdiff_t bufferMemoryOffset() { return Base::bufferMemoryOffset(); }
[[nodiscard]] size_t capacity() const { return Base::capacity(); }
[[nodiscard]] bool isEmpty() const { return !size(); }
[[nodiscard]] std::span<const T> span() const LIFETIME_BOUND { return { data(), size() }; }
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: 杉山 壮太, lattice, Behzad Najjarpour Jabbari (@G4ru), Junyeong Lee, Mooth.ai, OGINOME Tomohito, Using GLM From Z.AI, Gia Bui (@yabeow) from Calif.io
Key insight. The dropdown button outlived its owner because it held a raw reference to the TextFieldInputType, which the type change destroyed while the button could still receive the click. The fix converts the owner to a WeakPtr, nulls it on shadow-subtree teardown, and upgrades to a RefPtr across the callback — so the button never touches a destroyed owner.
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: An anonymous researcher
Background
Advisory: Apple Security Advisory
| Attribute | Value |
|---|---|
| CVE | CVE-2026-28979 |
| Bugzilla | 317324 |
| Component | WebKit |
| Bug Class | OOB |
| Severity | medium |
| Advisory | Apple Advisory |
Fix not public. The WebKit fix for this bug is embargoed or not yet disclosed. The bug ID is confirmed from the Apple advisory, but no public commit references it.
Root Cause Analysis
An out-of-bounds access issue was addressed with improved bounds checking.
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: Utkarsh Pal, Ignacio Sanmillan (@ulexec)
Key insight. Real-time audio DSP state must never be a JS-heap object shared with and concurrently dereferenced by the main thread; the root cause was storing the shaping curve as a Float32Array reachable from both threads (with an unlocked ‘main-thread-only’ getter), and the fix is to copy it into an engine-owned Vector<float> so nothing crosses the thread boundary as a live JS cell.
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: Using GLM From Z.AI, Tristan Madani (@TristanInSec) from Talence Security, stratan (@5tratan) of Almamater Technologies, Soyeon Park, Amy Burnett, Khai Tran, sherkito, Kota Toda, HexRabbit (@h3xr4bb1t) and NiNi (@terrynini38514) of DEVCORE Research Team, Brian Carpenter
Key insight. Constant folding may only bake in state that is immutable for the life of the compiled code; typed arrays over resizable/growable-shared buffers break that assumption because resize()/grow() can relocate the backing store, so the fix is to refuse to fold on such views rather than to track their relocation.
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: Tommy DeVoss from Braze Security Team (@thedawgyg), Mateusz Krzywicki (iVerify.io), dr3dd
Background
Advisory: Apple Security Advisory
| Attribute | Value |
|---|---|
| CVE | CVE-2026-43676 |
| Bugzilla | 317231 |
| Component | WebKit |
| Bug Class | OOB |
| Severity | medium |
| Advisory | Apple Advisory |
Fix not public. The WebKit fix for this bug is embargoed or not yet disclosed. The bug ID is confirmed from the Apple advisory, but no public commit references it.
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: Tommy DeVoss from Braze Security Team (@thedawgyg)
Background
Advisory: Apple Security Advisory
| Attribute | Value |
|---|---|
| CVE | CVE-2026-43699 |
| Bugzilla | 317227 |
| Component | WebKit |
| Bug Class | UAF |
| Severity | medium |
| Advisory | Apple Advisory |
Fix not public. The WebKit fix for this bug is embargoed or not yet disclosed. The bug ID is confirmed from the Apple advisory, but no public commit references it.
Overview
Impact:
Description: Processing maliciously crafted web content may disclose sensitive user information
Researchers: Vitaly Simonovich, Muhamad Syaiful, Christian Meurer Xavier
Key insight. importExternalTexture enforced the cross-origin video taint check on only some paths; adding it to the reused-external-texture fast path and the main path closes the remaining SOP holes.
Patch Details
diff --git a/LayoutTests/fast/webgpu/regression/repro_315368-expected.txt b/LayoutTests/fast/webgpu/regression/repro_315368-expected.txt
new file mode 100644
index 000000000000..f3f8a6a97a34
--- /dev/null
+++ b/LayoutTests/fast/webgpu/regression/repro_315368-expected.txt
@@ -0,0 +1,2 @@
+CONSOLE MESSAGE: Pass
+
diff --git a/LayoutTests/fast/webgpu/regression/repro_315368.html b/LayoutTests/fast/webgpu/regression/repro_315368.html
new file mode 100644
index 000000000000..cecbedb144ce
--- /dev/null
+++ b/LayoutTests/fast/webgpu/regression/repro_315368.html
@@ -0,0 +1,29 @@
+<script>
+ globalThis.testRunner?.waitUntilDone();
+ const log = console.debug;
+
+ const veryBrightVideo = `data:video/mp4;base64,AAAAHGZ0eXBpc29tAAACAGlzb21pc28ybXA0MQAAAAhmcmVlAAAAvG1kYXQAAAAfTgEFGkdWStxcTEM/lO/FETzRQ6gD7gAA7gIAA3EYgAAAAEgoAa8iNjAkszOL+e58c//cEe//0TT//scp1n/381P/RWP/zOW4QtxorfVogeh8nQDbQAAAAwAQMCcWUTAAAAMAAAMAAAMA84AAAAAVAgHQAyu+KT35E7gAADFgAAADABLQAAAAEgIB4AiS76MTkNbgAAF3AAAPSAAAABICAeAEn8+hBOTXYAADUgAAHRAAAAPibW9vdgAAAGxtdmhkAAAAAAAAAAAAAAAAAAAD6AAAAKcAAQAAAQAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAw10cmFrAAAAXHRraGQAAAADAAAAAAAAAAAAAAABAAAAAAAAAKcAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAABAAAAAQAAAAAAAkZWR0cwAAABxlbHN0AAAAAAAAAAEAAACnAAAAAAABAAAAAAKFbWRpYQAAACBtZGhkAAAAAAAAAAAAAAAAAABdwAAAD6BVxAAAAAAAMWhkbHIAAAAAAAAAAHZpZGUAAAAAAAAAAAAAAABDb3JlIE1lZGlhIFZpZGVvAAAAAixtaW5mAAAAFHZtaGQAAAABAAAAAAAAAAAAAAAkZGluZgAAABxkcmVmAAAAAAAAAAEAAAAMdXJsIAAAAAEAAAHsc3RibAAAARxzdHNkAAAAAAAAAAEAAAEMaHZjMQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAQABAASAAAAEgAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABj//wAAAHVodmNDAQIgAAAAsAAAAAAAPPAA/P36+gAACwOgAAEAGEABDAH//wIgAAADALAAAAMAAAMAPBXAkKEAAQAmQgEBAiAAAAMAsAAAAwAAAwA8oBQgQcCTDLYgV7kWVYC1CRAJAICiAAEACUQBwChkuNBTJAAAAApmaWVsAQAAAAATY29scm5jbHgACQAQAAkAAAAAEHBhc3AAAAABAAAAAQAAABRidHJ0AAAAAAAALPwAACz8AAAAKHN0dHMAAAAAAAAAAwAAAAIAAAPoAAAAAQAAAAEAAAABAAAD6AAAABRzdHNzAAAAAAAAAAEAAAABAAAAEHNkdHAAAAAAIBAQGAAAAChjdHRzAAAAAAAAAAMAAAABAAAAAAAAAAEAAAfQAAAAAgAAAAAAAAAcc3RzYwAAAAAAAAABAAAAAQAAAAQAAAABAAAAJHN0c3oAAAAAAAAAAAAAAAQAAABvAAAAGQAAABYAAAAWAAAAFHN0Y28AAAAAAAAAAQAAACwAAABhdWR0YQAAAFltZXRhAAAAAAAAACFoZGxyAAAAAAAAAABtZGlyYXBwbAAAAAAAAAAAAAAAACxpbHN0AAAAJKl0b28AAAAcZGF0YQAAAAEAAAAATGF2ZjYwLjMuMTAw`;
+
+ // Tests the new texture path in importExternalTexture: first import of a same-origin video
+ // element exercises the code path that creates a new GPUExternalTexture (bug 315368).
+ onload = async () => {
+ let adapter = await navigator.gpu.requestAdapter({});
+ let device = await adapter.requestDevice({});
+
+ let video = document.createElement('video');
+ video.src = veryBrightVideo;
+ video.muted = true;
+ await new Promise(resolve => { video.onloadeddata = () => { video.play(); resolve(); }; });
+
+ video.requestVideoFrameCallback(() => {
+ try {
+ device.importExternalTexture({source: video});
+ log('Pass');
+ } catch (e) {
+ log('FAIL: ' + e.message);
+ }
+ globalThis.testRunner?.dumpAsText();
+ globalThis.testRunner?.notifyDone();
+ });
+ };
+</script>
diff --git a/LayoutTests/fast/webgpu/regression/repro_315368b-expected.txt b/LayoutTests/fast/webgpu/regression/repro_315368b-expected.txt
new file mode 100644
index 000000000000..f3f8a6a97a34
--- /dev/null
+++ b/LayoutTests/fast/webgpu/regression/repro_315368b-expected.txt
@@ -0,0 +1,2 @@
+CONSOLE MESSAGE: Pass
+
diff --git a/LayoutTests/fast/webgpu/regression/repro_315368b.html b/LayoutTests/fast/webgpu/regression/repro_315368b.html
new file mode 100644
index 000000000000..b5efaa958908
--- /dev/null
+++ b/LayoutTests/fast/webgpu/regression/repro_315368b.html
@@ -0,0 +1,31 @@
+<script>
+ globalThis.testRunner?.waitUntilDone();
+ const log = console.debug;
+
+ const veryBrightVideo = `data:video/mp4;base64,AAAAHGZ0eXBpc29tAAACAGlzb21pc28ybXA0MQAAAAhmcmVlAAAAvG1kYXQAAAAfTgEFGkdWStxcTEM/lO/FETzRQ6gD7gAA7gIAA3EYgAAAAEgoAa8iNjAkszOL+e58c//cEe//0TT//scp1n/381P/RWP/zOW4QtxorfVogeh8nQDbQAAAAwAQMCcWUTAAAAMAAAMAAAMA84AAAAAVAgHQAyu+KT35E7gAADFgAAADABLQAAAAEgIB4AiS76MTkNbgAAF3AAAPSAAAABICAeAEn8+hBOTXYAADUgAAHRAAAAPibW9vdgAAAGxtdmhkAAAAAAAAAAAAAAAAAAAD6AAAAKcAAQAAAQAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAw10cmFrAAAAXHRraGQAAAADAAAAAAAAAAAAAAABAAAAAAAAAKcAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAABAAAAAQAAAAAAAkZWR0cwAAABxlbHN0AAAAAAAAAAEAAACnAAAAAAABAAAAAAKFbWRpYQAAACBtZGhkAAAAAAAAAAAAAAAAAABdwAAAD6BVxAAAAAAAMWhkbHIAAAAAAAAAAHZpZGUAAAAAAAAAAAAAAABDb3JlIE1lZGlhIFZpZGVvAAAAAixtaW5mAAAAFHZtaGQAAAABAAAAAAAAAAAAAAAkZGluZgAAABxkcmVmAAAAAAAAAAEAAAAMdXJsIAAAAAEAAAHsc3RibAAAARxzdHNkAAAAAAAAAAEAAAEMaHZjMQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAQABAASAAAAEgAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABj//wAAAHVodmNDAQIgAAAAsAAAAAAAPPAA/P36+gAACwOgAAEAGEABDAH//wIgAAADALAAAAMAAAMAPBXAkKEAAQAmQgEBAiAAAAMAsAAAAwAAAwA8oBQgQcCTDLYgV7kWVYC1CRAJAICiAAEACUQBwChkuNBTJAAAAApmaWVsAQAAAAATY29scm5jbHgACQAQAAkAAAAAEHBhc3AAAAABAAAAAQAAABRidHJ0AAAAAAAALPwAACz8AAAAKHN0dHMAAAAAAAAAAwAAAAIAAAPoAAAAAQAAAAEAAAABAAAD6AAAABRzdHNzAAAAAAAAAAEAAAABAAAAEHNkdHAAAAAAIBAQGAAAAChjdHRzAAAAAAAAAAMAAAABAAAAAAAAAAEAAAfQAAAAAgAAAAAAAAAcc3RzYwAAAAAAAAABAAAAAQAAAAQAAAABAAAAJHN0c3oAAAAAAAAAAAAAAAQAAABvAAAAGQAAABYAAAAWAAAAFHN0Y28AAAAAAAAAAQAAACwAAABhdWR0YQAAAFltZXRhAAAAAAAAACFoZGxyAAAAAAAAAABtZGlyYXBwbAAAAAAAAAAAAAAAACxpbHN0AAAAJKl0b28AAAAcZGF0YQAAAAEAAAAATGF2ZjYwLjMuMTAw`;
+
+ // Tests the cached texture path in importExternalTexture: a second synchronous import of the
+ // same same-origin video element exercises the early-return path that reuses a cached
+ // GPUExternalTexture (bug 315368, PLATFORM(COCOA) only).
+ onload = async () => {
+ let adapter = await navigator.gpu.requestAdapter({});
+ let device = await adapter.requestDevice({});
+
+ let video = document.createElement('video');
+ video.src = veryBrightVideo;
+ video.muted = true;
+ await new Promise(resolve => { video.onloadeddata = () => { video.play(); resolve(); }; });
+
+ video.requestVideoFrameCallback(() => {
+ try {
+ let et1 = device.importExternalTexture({source: video}); // new texture path
+ let et2 = device.importExternalTexture({source: video}); // cached texture path (Cocoa)
+ log('Pass');
+ } catch (e) {
+ log('FAIL: ' + e.message);
+ }
+ globalThis.testRunner?.dumpAsText();
+ globalThis.testRunner?.notifyDone();
+ });
+ };
+</script>
diff --git a/Source/WebCore/Modules/WebGPU/GPUDevice.cpp b/Source/WebCore/Modules/WebGPU/GPUDevice.cpp
index add1e02b8303..3d03ba423a61 100644
--- a/Source/WebCore/Modules/WebGPU/GPUDevice.cpp
+++ b/Source/WebCore/Modules/WebGPU/GPUDevice.cpp
@@ -74,6 +74,7 @@
#include "JSGPUUncapturedErrorEvent.h"
#include "JSGPUValidationError.h"
#include "RequestAnimationFrameCallback.h"
+#include "SecurityOrigin.h"
#include "WebGPUXRBinding.h"
#include "XRGPUBinding.h"
#include <wtf/TZoneMallocInlines.h>
@@ -394,6 +395,15 @@ private:
ExceptionOr<Ref<GPUExternalTexture>> GPUDevice::importExternalTexture(GPUExternalTextureDescriptor&& externalTextureDescriptor)
{
+#if ENABLE(VIDEO)
+ auto checkVideoElementOriginTaint = [this](const HTMLVideoElement& videoElement) -> std::optional<Exception> {
+ RefPtr context = scriptExecutionContext();
+ if (RefPtr securityOrigin = context ? context->securityOrigin() : nullptr; securityOrigin && videoElement.taintsOrigin(*securityOrigin))
+ return Exception { ExceptionCode::SecurityError, "GPUDevice.importExternalTexture: Cross origin external videos are not allowed in WebGPU"_s };
+ return std::nullopt;
+ };
+#endif
+
#if ENABLE(VIDEO) && PLATFORM(COCOA)
if (RefPtr externalTexture = externalTextureForDescriptor(externalTextureDescriptor)) {
externalTexture->undestroy();
@@ -402,6 +412,9 @@ ExceptionOr<Ref<GPUExternalTexture>> GPUDevice::importExternalTexture(GPUExterna
#else
Ref videoElementRef = externalTextureDescriptor.source;
#endif
+ if (auto exception = checkVideoElementOriginTaint(videoElementRef))
+ return WTF::move(*exception);
+
m_videoElementToExternalTextureMap.remove(videoElementRef);
if (auto optionalMediaIdentifier = externalTextureDescriptor.mediaIdentifier()) {
m_backing->updateExternalTexture(externalTexture->backing(), *optionalMediaIdentifier);
@@ -425,6 +438,9 @@ ExceptionOr<Ref<GPUExternalTexture>> GPUDevice::importExternalTexture(GPUExterna
#else
Ref videoElementRef = externalTextureDescriptor.source;
#endif
+ if (auto exception = checkVideoElementOriginTaint(videoElementRef))
+ return WTF::move(*exception);
+
WeakPtr videoElementPtr = videoElementRef.ptr();
m_videoElementToExternalTextureMap.set(videoElementRef, externalTexture.get());
m_previouslyImportedExternalTexture.first = videoElementRef.ptr();
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: A malicious website may be able to process restricted web content outside the sandbox
Researchers: Aaron Grattafiori - NVIDIA AI Red Team
Key insight. A data: URL response should only be downloadable when initiated by the user or API client; relying on stock ‘download unshowable responses’ let web content write files silently.
Patch Details
diff --git a/Source/WebCore/loader/DocumentLoader.cpp b/Source/WebCore/loader/DocumentLoader.cpp
index 9ee9f3231e57..d757709c2535 100644
--- a/Source/WebCore/loader/DocumentLoader.cpp
+++ b/Source/WebCore/loader/DocumentLoader.cpp
@@ -1142,6 +1142,16 @@ void DocumentLoader::continueAfterContentPolicy(PolicyAction policy)
return;
}
+ // Defense-in-depth: refuse to download a data: URL through a top-frame navigation that
+ // wasn't initiated by the user or the API client, mirroring the existing check in the
+ // PolicyAction::Use branch. The primary defense lives in the UI process; this guards
+ // ports / future flows that don't share that boundary.
+ if (disallowDataRequest()) {
+ protect(frameLoader())->policyChecker().cannotShowMIMEType(m_response);
+ stopLoadingForPolicyChange();
+ return;
+ }
+
if (RefPtr mainResourceLoader = this->mainResourceLoader())
InspectorInstrumentation::continueWithPolicyDownload(*frame, *mainResourceLoader->identifier(), *this, m_response);
diff --git a/Source/WebKit/UIProcess/API/APINavigation.h b/Source/WebKit/UIProcess/API/APINavigation.h
index 75076f101b82..69ca01776969 100644
--- a/Source/WebKit/UIProcess/API/APINavigation.h
+++ b/Source/WebKit/UIProcess/API/APINavigation.h
@@ -132,6 +132,7 @@ class Navigation : public ObjectImpl<Object::Type::Navigation> {
bool wasUserInitiated() const { return m_lastNavigationAction && !!m_lastNavigationAction->userGestureTokenIdentifier; }
bool NODELETE isRequestFromClientOrUserInput() const;
+ bool isFromAPIClientRequest() const { return m_requestIsFromClientInput; }
void NODELETE markRequestAsFromClientInput();
void markAsFromLoadData() { m_isFromLoadData = true; }
bool isFromLoadData() const { return m_isFromLoadData; }
diff --git a/Source/WebKit/UIProcess/WebPageProxy.cpp b/Source/WebKit/UIProcess/WebPageProxy.cpp
index 572ea8d6537f..ceee9ead3799 100644
--- a/Source/WebKit/UIProcess/WebPageProxy.cpp
+++ b/Source/WebKit/UIProcess/WebPageProxy.cpp
@@ -5924,6 +5924,19 @@ void WebPageProxy::receivedNavigationResponsePolicyDecision(WebCore::PolicyActio
if (!hasRunningProcess())
return completionHandler(PolicyDecision { });
+ // Refuse to convert a navigation response into a download when the request is a data: URL,
+ // unless the navigation was driven by the API client (e.g. -loadRequest: with a data: URL).
+ // Otherwise a compromised Web Content process could navigate to a data: URL with an unshowable
+ // MIME type and rely on the navigation delegate's stock "download unshowable responses"
+ // behavior to write attacker-controlled bytes to disk without user interaction. Legitimate
+ // downloads of data: URLs go through the navigation action policy (e.g. <a href="data:..." download>)
+ // or the explicit download API, neither of which reaches this code path.
+ if (action == PolicyAction::Download && request.url().protocolIsData()
+ && (!navigation || !navigation->isFromAPIClientRequest())) {
+ WEBPAGEPROXY_RELEASE_LOG(Loading, "receivedNavigationResponsePolicyDecision: refusing to download data: URL not initiated by API client");
+ action = PolicyAction::Ignore;
+ }
+
Ref pageLoadState = internals().pageLoadState;
auto transaction = pageLoadState->transaction();
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: A malicious web extension may be able to cause an unexpected process crash
Researchers: dr3dd
Key insight. Extension event dispatch iterated the live listener list by reference while callbacks could add/remove listeners; copying the list and retaining each listener across call() removes the iterator-invalidation UAF.
Patch Details
diff --git a/Source/WebKit/WebProcess/Extensions/API/Cocoa/WebExtensionAPIDevToolsExtensionPanelCocoa.mm b/Source/WebKit/WebProcess/Extensions/API/Cocoa/WebExtensionAPIDevToolsExtensionPanelCocoa.mm
index b67b1520577e..68d947e9071b 100644
--- a/Source/WebKit/WebProcess/Extensions/API/Cocoa/WebExtensionAPIDevToolsExtensionPanelCocoa.mm
+++ b/Source/WebKit/WebProcess/Extensions/API/Cocoa/WebExtensionAPIDevToolsExtensionPanelCocoa.mm
@@ -74,7 +74,10 @@ void WebExtensionContextProxy::dispatchDevToolsExtensionPanelShownEvent(Inspecto
if (!extensionPanel)
return;
- for (auto& listener : extensionPanel->onShown().listeners()) {
+ // Copy the listeners since call() can trigger a mutation of the listeners.
+ auto listenersCopy = extensionPanel->onShown().listeners();
+
+ for (RefPtr listener : listenersCopy) {
auto globalContext = listener->globalContext();
listener->call(toWindowObject(globalContext, *frame));
}
diff --git a/Source/WebKit/WebProcess/Extensions/API/Cocoa/WebExtensionAPIPortCocoa.mm b/Source/WebKit/WebProcess/Extensions/API/Cocoa/WebExtensionAPIPortCocoa.mm
index b5fedd5193ed..e076b223d97b 100644
--- a/Source/WebKit/WebProcess/Extensions/API/Cocoa/WebExtensionAPIPortCocoa.mm
+++ b/Source/WebKit/WebProcess/Extensions/API/Cocoa/WebExtensionAPIPortCocoa.mm
@@ -161,7 +161,10 @@ void WebExtensionAPIPort::fireMessageEventIfNeeded(id message, bool userGesture)
RELEASE_LOG_DEBUG(Extensions, "Fired port message event for channel %{public}llu in %{public}@ world", channelIdentifier().toUInt64(), toDebugString(contentWorldType()).createNSString().get());
- for (auto& listener : m_onMessage->listeners()) {
+ // Copy the listeners since call() can trigger a mutation of the listeners.
+ auto listenersCopy = m_onMessage->listeners();
+
+ for (RefPtr listener : listenersCopy) {
auto globalContext = listener->globalContext();
std::optional<WebCore::UserGestureIndicator> gestureIndicator;
@@ -197,7 +200,10 @@ void WebExtensionAPIPort::fireDisconnectEventIfNeeded()
RELEASE_LOG_DEBUG(Extensions, "Fired port disconnect event for channel %{public}llu in %{public}@ world", m_channelIdentifier ? m_channelIdentifier->toUInt64() : 0, toDebugString(contentWorldType()).createNSString().get());
- for (auto& listener : m_onDisconnect->listeners()) {
+ // Copy the listeners since call() can trigger a mutation of the listeners.
+ auto listenersCopy = m_onDisconnect->listeners();
+
+ for (RefPtr listener : listenersCopy) {
auto globalContext = listener->globalContext();
listener->call(toJS(globalContext, this));
diff --git a/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKWebExtensionAPIRuntime.mm b/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKWebExtensionAPIRuntime.mm
index 36df19179b7d..0af1f19888e2 100644
--- a/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKWebExtensionAPIRuntime.mm
+++ b/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKWebExtensionAPIRuntime.mm
@@ -1724,6 +1724,57 @@ TEST(WKWebExtensionAPIRuntime, ConnectNative)
[manager run];
}
+TEST(WKWebExtensionAPIRuntime, PortMutateListenersDuringDispatch)
+{
+ auto *backgroundScript = Util::constructScript(@[
+ @"const port = browser.runtime?.connectNative('test')",
+
+ @"const initialListenerCount = 64",
+ @"let messageDispatchCount = 0",
+ @"let disconnectDispatchCount = 0",
+
+ @"port?.onMessage.addListener(() => {",
+ @" ++messageDispatchCount",
+ @" for (let i = 0; i < 1024; ++i)",
+ @" port?.onMessage.addListener(() => { })",
+ @"})",
+
+ @"for (let i = 0; i < initialListenerCount; ++i)",
+ @" port?.onMessage.addListener(() => { ++messageDispatchCount })",
+
+ @"port?.onDisconnect.addListener(() => {",
+ @" ++disconnectDispatchCount",
+ @" for (let i = 0; i < 1024; ++i)",
+ @" port?.onDisconnect.addListener(() => { })",
+ @"})",
+
+ @"for (let i = 0; i < initialListenerCount; ++i)",
+ @" port?.onDisconnect.addListener(() => { ++disconnectDispatchCount })",
+
+ @"port?.onDisconnect.addListener(() => {",
+ @" ++disconnectDispatchCount",
+ @" browser.test.assertEq(messageDispatchCount, initialListenerCount + 1, 'Should dispatch to the listeners registered when onMessage fired')",
+ @" browser.test.assertEq(disconnectDispatchCount, initialListenerCount + 2, 'Should dispatch to the listeners registered when onDisconnect fired')",
+ @" browser.test.notifyPass()",
+ @"})",
+
+ @"port?.postMessage('Hello')"
+ ]);
+
+ auto manager = Util::loadExtension(runtimeManifest, @{ @"background.js": backgroundScript });
+
+ [manager.get().context setPermissionStatus:WKWebExtensionContextPermissionStatusGrantedExplicitly forPermission:WKWebExtensionPermissionNativeMessaging];
+
+ manager.get().internalDelegate.connectUsingMessagePort = ^(WKWebExtensionMessagePort *messagePort) {
+ messagePort.messageHandler = ^(id message, NSError *error) {
+ [messagePort sendMessage:@"Received" completionHandler:^(NSError *) { }];
+ [messagePort disconnectWithError:nil];
+ };
+ };
+
+ [manager run];
+}
+
TEST(WKWebExtensionAPIRuntime, ConnectNativeWithInvalidMessage)
{
auto *backgroundScript = Util::constructScript(@[
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to memory corruption
Researchers: dr3dd
Key insight. Values returned across a JS-iterable boundary (Array Symbol.iterator) were trusted for both count and type: a compiled-out ASSERT plus unchecked dynamicDowncasts let a poisoned iterator substitute arbitrary objects for the readable/writable streams.
Patch Details
diff --git a/LayoutTests/streams/transform-stream-poisoned-iterator-crash-expected.txt b/LayoutTests/streams/transform-stream-poisoned-iterator-crash-expected.txt
new file mode 100644
index 000000000000..fe4368d722cb
--- /dev/null
+++ b/LayoutTests/streams/transform-stream-poisoned-iterator-crash-expected.txt
@@ -0,0 +1,6 @@
+Tests that constructing a TransformStream does not crash when Array.prototype[Symbol.iterator] has been poisoned to return wrong-type objects.
+WebKit should not crash, and you should see PASS below.
+
+PASS: plain-object substitution threw TypeError
+PASS: object-reference substitution threw TypeError
+PASS: truncated iterator threw TypeError
diff --git a/LayoutTests/streams/transform-stream-poisoned-iterator-crash.html b/LayoutTests/streams/transform-stream-poisoned-iterator-crash.html
new file mode 100644
index 000000000000..3c7d91bbcded
--- /dev/null
+++ b/LayoutTests/streams/transform-stream-poisoned-iterator-crash.html
@@ -0,0 +1,88 @@
+<!DOCTYPE html>
+<html>
+<body>
+<p>Tests that constructing a TransformStream does not crash when Array.prototype[Symbol.iterator] has been poisoned to return wrong-type objects.<br>
+WebKit should not crash, and you should see PASS below.</p>
+<div id="result"></div>
+<script>
+if (window.testRunner) {
+ testRunner.dumpAsText();
+ testRunner.waitUntilDone();
+}
+
+// createInternalTransformStreamFromTransformer returns a 3-element array
+// that the C++ side iterates with convert<IDLSequence<IDLObject>>. By
+// poisoning Array.prototype[Symbol.iterator] we can substitute arbitrary
+// JS objects for the readable/writable entries. Without a runtime check,
+// the C++ side then treats those objects as JSReadableStream/JSWritableStream
+// and dereferences attacker-controlled memory.
+
+function check(label, fn) {
+ let message;
+ try {
+ fn();
+ message = "FAIL: " + label + " did not throw";
+ } catch (e) {
+ if (e instanceof TypeError)
+ message = "PASS: " + label + " threw TypeError";
+ else
+ message = "FAIL: " + label + " threw " + e;
+ }
+ const div = document.createElement("div");
+ div.textContent = message;
+ document.getElementById("result").appendChild(div);
+}
+
+const originalIterator = Array.prototype[Symbol.iterator];
+
+// Variant 1: substitute plain objects for the readable/writable slots.
+Array.prototype[Symbol.iterator] = function() {
+ const arr = this;
+ let i = 0;
+ return {
+ next() {
+ if (i >= arr.length)
+ return { done: true };
+ let val = arr[i];
+ if (arr.length === 3 && i >= 1)
+ val = { fake: true };
+ i++;
+ return { value: val, done: false };
+ }
+ };
+};
+check("plain-object substitution", () => new TransformStream());
+
+// Variant 2: substitute an object that holds a heap pointer (would survive
+// the initial dereference and trigger the write-increment / vtable hijack).
+let buf = new ArrayBuffer(65536);
+Array.prototype[Symbol.iterator] = function() {
+ const arr = this;
+ let i = 0;
+ return {
+ next() {
+ if (i >= arr.length)
+ return { done: true };
+ let val = arr[i];
+ if (arr.length === 3 && i >= 1)
+ val = { pad: 0, ptr: buf };
+ i++;
+ return { value: val, done: false };
+ }
+ };
+};
+check("object-reference substitution", () => new TransformStream());
+
+// Variant 3: truncated iterator returning fewer than 3 entries.
+Array.prototype[Symbol.iterator] = function() {
+ return { next() { return { done: true }; } };
+};
+check("truncated iterator", () => new TransformStream());
+
+Array.prototype[Symbol.iterator] = originalIterator;
+
+if (window.testRunner)
+ testRunner.notifyDone();
+</script>
+</body>
+</html>
diff --git a/Source/WebCore/Modules/streams/TransformStream.cpp b/Source/WebCore/Modules/streams/TransformStream.cpp
index 3098085811fa..3694d6c2be6f 100644
--- a/Source/WebCore/Modules/streams/TransformStream.cpp
+++ b/Source/WebCore/Modules/streams/TransformStream.cpp
@@ -166,9 +166,15 @@ ExceptionOr<CreateInternalTransformStreamResult> createInternalTransformStream(J
return Exception { ExceptionCode::ExistingExceptionError };
auto results = resultsConversionResult.releaseReturnValue();
- ASSERT(results.size() == 3);
+ if (results.size() != 3) [[unlikely]]
+ return Exception { ExceptionCode::TypeError, "Internal TransformStream creation returned an unexpected number of values"_s };
- return CreateInternalTransformStreamResult { results[0].get(), dynamicDowncast<JSReadableStream>(results[1].get())->wrapped(), dynamicDowncast<JSWritableStream>(results[2].get())->wrapped() };
+ auto* readable = dynamicDowncast<JSReadableStream>(results[1].get());
+ auto* writable = dynamicDowncast<JSWritableStream>(results[2].get());
+ if (!readable || !writable) [[unlikely]]
+ return Exception { ExceptionCode::TypeError, "Internal TransformStream creation returned values of unexpected types"_s };
+
+ return CreateInternalTransformStreamResult { results[0].get(), readable->wrapped(), writable->wrapped() };
}
template<typename Visitor>
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: stratan (@5tratan) of Almamater Technologies, OpenAI Codex Security - Amy Burnett
Background
Advisory: Apple Security Advisory
| Attribute | Value |
|---|---|
| CVE | CVE-2026-43707 |
| Bugzilla | 315951 |
| Component | WebKit |
| Bug Class | LogicError |
| Severity | medium |
| Advisory | Apple Advisory |
Fix not public. The WebKit fix for this bug is embargoed or not yet disclosed. The bug ID is confirmed from the Apple advisory, but no public commit references it.
Overview
Impact:
Description: A malicious website may exfiltrate data cross-origin
Researchers: Behzad Najjarpour Jabbari (@G4ru)
Key insight. The partitioned-cookie guard tested a lazily-created request that could be nil, so -respondsToSelector: returned NO and WebSocket requests silently skipped third-party cookie blocking; materializing the request via ensureMutableRequest() applies the policy.
Patch Details
diff --git a/Source/WebKit/NetworkProcess/cocoa/NetworkSessionCocoa.mm b/Source/WebKit/NetworkProcess/cocoa/NetworkSessionCocoa.mm
index 7fc3aaf13384..e9ab1470b2e7 100644
--- a/Source/WebKit/NetworkProcess/cocoa/NetworkSessionCocoa.mm
+++ b/Source/WebKit/NetworkProcess/cocoa/NetworkSessionCocoa.mm
@@ -1693,7 +1693,7 @@ RefPtr<WebSocketTask> NetworkSessionCocoa::createWebSocketTask(WebPageProxyIdent
ensureMutableRequest().get()._privacyProxyFailClosedForUnreachableNonMainHosts = YES;
#if ENABLE(OPT_IN_PARTITIONED_COOKIES) && defined(CFN_COOKIE_ACCEPTS_POLICY_PARTITION) && CFN_COOKIE_ACCEPTS_POLICY_PARTITION
- if ([mutableRequest respondsToSelector:@selector(_setAllowOnlyPartitionedCookies:)]) {
+ if ([ensureMutableRequest() respondsToSelector:@selector(_setAllowOnlyPartitionedCookies:)]) {
if (CheckedPtr storageSession = networkStorageSession(); storageSession && storageSession->isOptInCookiePartitioningEnabled()) {
bool shouldAllowOnlyPartitioned = storageSession->thirdPartyCookieBlockingDecisionForRequest(request, frameID, pageID, networkProcess().shouldRelaxThirdPartyCookieBlockingForPage(webPageProxyID), isRequestToKnownCrossSiteTracker(request)) == WebCore::ThirdPartyCookieBlockingDecision::AllExceptPartitioned;
[mutableRequest _setAllowOnlyPartitionedCookies:shouldAllowOnlyPartitioned];
diff --git a/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKHTTPCookieStore.mm b/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKHTTPCookieStore.mm
index 0f29bd2dfe83..8e19192730f0 100644
--- a/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKHTTPCookieStore.mm
+++ b/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKHTTPCookieStore.mm
@@ -836,6 +836,50 @@ TEST(WKHTTPCookieStore, DISABLED_WebSocketCookies)
Util::run(&receivedThirdRequest);
}
+TEST(WKHTTPCookieStore, WebSocketCookies2)
+{
+ using namespace TestWebKitAPI;
+ bool receivedThirdRequest { false };
+ HTTPServer server(TestWebKitAPI::HTTPServer::UseCoroutines::Yes, [&] (Connection connection) -> ConnectionTask { while (true) {
+ auto request = co_await connection.awaitableReceiveHTTPRequest();
+ auto path = HTTPServer::parsePath(request);
+ request.append(0);
+ if (path == "http://sitea.example/com"_s) {
+ co_await connection.awaitableSend(
+ "HTTP/1.1 200 OK\r\n"
+ "Content-Length: 0\r\n"
+ "Set-Cookie: Default=1\r\n"
+ "Set-Cookie: SameSite_None=1; SameSite=None\r\n"
+ "Set-Cookie: SameSite_None_Secure=1; secure; SameSite=None\r\n"
+ "Set-Cookie: SameSite_Lax=1; SameSite=Lax\r\n"
+ "Set-Cookie: SameSite_Strict=1; SameSite=Strict\r\n"
+ "\r\n"_s);
+ } else if (path == "ws://sitea.example/websocket"_s) {
+ EXPECT_TRUE(contains(request.span(), "Host: sitea.example"_span));
+ EXPECT_FALSE(contains(request.span(), "Cookie:"_span));
+ receivedThirdRequest = true;
+ } else if (path == "http://siteb.example/ninja"_s) {
+ auto html = @"<script>new WebSocket('ws://siteA.example/websocket')</script>";
+ co_await connection.awaitableSend(HTTPResponse(html).serialize());
+ } else
+ EXPECT_WK_STREQ(@"SHOULD NOT BE REACH", path);
+ } });
+
+ RetainPtr storeConfiguration = adoptNS([[_WKWebsiteDataStoreConfiguration alloc] initNonPersistentConfiguration]);
+ [storeConfiguration setHTTPProxy:[NSURL URLWithString:[NSString stringWithFormat:@"https://127.0.0.1:%d/", server.port()]]];
+ RetainPtr dataStore = adoptNS([[WKWebsiteDataStore alloc] _initWithConfiguration:storeConfiguration.get()]);
+ RetainPtr viewConfiguration = adoptNS([WKWebViewConfiguration new]);
+ [viewConfiguration setWebsiteDataStore:dataStore.get()];
+
+ RetainPtr webView = adoptNS([[WKWebView alloc] initWithFrame:CGRectZero configuration:viewConfiguration.get()]);
+ [[[webView configuration] websiteDataStore] _setResourceLoadStatisticsEnabled:YES];
+ [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://siteA.example/com"]]];
+ [webView _test_waitForDidFinishNavigation];
+
+ [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://siteB.example/ninja"]]];
+ Util::run(&receivedThirdRequest);
+}
+
TEST(WKHTTPCookieStore, WebSocketCookiesFromRedirect)
{
using namespace TestWebKitAPI;
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: An anonymous researcher
Key insight. ContextMtl kept a raw QueryMtl* whose lifetime was not tied to the render pass that dereferenced it. The fix removes the object pointer entirely and instead holds a strong reference to the query’s ref-counted visibility-result buffer for the duration it is needed — turning a lifetime assumption into an enforced ownership guarantee.
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: Kwak Kiyong, Song nuri, Tristan Madani (@TristanInSec) from Talence Security
Key insight. Retaining only the directly-named TypeDefinition — not the transitive closure of types it references — left dangling TypeIndex values in long-lived Globals/Tables once the defining module was collected.
Patch Details
diff --git a/JSTests/wasm/gc/transitive-type-retention-global.js b/JSTests/wasm/gc/transitive-type-retention-global.js
new file mode 100644
index 000000000000..ff5d2608adf8
--- /dev/null
+++ b/JSTests/wasm/gc/transitive-type-retention-global.js
@@ -0,0 +1,24 @@
+// Regression test: Wasm Global must retain transitive TypeDefinition dependencies.
+// A global typed (ref null $f) where $f references struct type $s must keep $s alive
+// even after the module is collected.
+
+import { instantiate } from "./wast-wrapper.js";
+
+(function() {
+ let inst = instantiate(`
+ (module
+ (rec (type $s (struct (field i32) (field i32) (field i32) (field i32))))
+ (rec (type $f (func (param (ref null $s)) (result (ref null $s)))))
+ (global (export "g") (mut (ref null $f)) (ref.null $f))
+ )
+ `);
+
+ let g = inst.exports.g;
+ inst = null;
+
+ gc();
+ gc();
+
+ g.value;
+ g.value = null;
+})();
diff --git a/JSTests/wasm/gc/transitive-type-retention-table.js b/JSTests/wasm/gc/transitive-type-retention-table.js
new file mode 100644
index 000000000000..30886e1a7692
--- /dev/null
+++ b/JSTests/wasm/gc/transitive-type-retention-table.js
@@ -0,0 +1,25 @@
+// Regression test: Wasm Table must retain transitive TypeDefinition dependencies.
+// A table typed (ref null $f) where $f references struct type $s must keep $s alive
+// even after the module is collected.
+
+import { instantiate } from "./wast-wrapper.js";
+
+(function() {
+ let inst = instantiate(`
+ (module
+ (rec (type $s (struct (field i32) (field i32) (field i32) (field i32))))
+ (rec (type $f (func (param (ref null $s)) (result (ref null $s)))))
+ (table (export "tbl") 10 10 (ref null $f))
+ )
+ `);
+
+ let tbl = inst.exports.tbl;
+ inst = null;
+
+ gc();
+ gc();
+
+ tbl.get(0);
+ tbl.set(0, null);
+ tbl.grow(0, null);
+})();
diff --git a/Source/JavaScriptCore/wasm/WasmGlobal.h b/Source/JavaScriptCore/wasm/WasmGlobal.h
index 08689a3bde2e..ae557c69fbb7 100644
--- a/Source/JavaScriptCore/wasm/WasmGlobal.h
+++ b/Source/JavaScriptCore/wasm/WasmGlobal.h
@@ -32,6 +32,7 @@
#include <JavaScriptCore/SlotVisitorMacros.h>
#include <JavaScriptCore/WasmFormat.h>
#include <JavaScriptCore/WasmLimits.h>
+#include <JavaScriptCore/WasmTypeDefinition.h>
#include <JavaScriptCore/WriteBarrier.h>
#include <wtf/Ref.h>
#include <wtf/TZoneMalloc.h>
@@ -99,25 +100,28 @@ public:
private:
Global(Wasm::Type type, Wasm::Mutability mutability, uint64_t initialValue)
: m_type(type)
- , m_typeDefinition(TypeInformation::getRef(type.index))
, m_mutability(mutability)
{
ASSERT(m_type != Types::V128);
+ if (auto typeDefinition = TypeInformation::getRef(type.index))
+ m_typeDependencies.emplace(Ref { *typeDefinition });
m_value.m_primitive = initialValue;
}
Global(Wasm::Type type, Wasm::Mutability mutability, v128_t initialValue)
: m_type(type)
- , m_typeDefinition(TypeInformation::getRef(type.index))
, m_mutability(mutability)
{
ASSERT(m_type == Types::V128);
+ if (auto typeDefinition = TypeInformation::getRef(type.index))
+ m_typeDependencies.emplace(Ref { *typeDefinition });
m_value.m_vector = initialValue;
}
Wasm::Type m_type;
- // If m_type came from a TypeDefinition, the following retains the definition to prevent a dangling m_type.
- RefPtr<const Wasm::TypeDefinition> m_typeDefinition;
+ // If m_type came from a TypeDefinition, the following retains the definition
+ // and all transitively reachable types to prevent dangling TypeIndex values.
+ std::optional<WebAssemblyGCTypeDependencies> m_typeDependencies;
Wasm::Mutability m_mutability;
JSWebAssemblyGlobal* m_owner { nullptr };
Value m_value;
diff --git a/Source/JavaScriptCore/wasm/WasmTable.cpp b/Source/JavaScriptCore/wasm/WasmTable.cpp
index be6b6df2e054..0a41b0da1de7 100644
--- a/Source/JavaScriptCore/wasm/WasmTable.cpp
+++ b/Source/JavaScriptCore/wasm/WasmTable.cpp
@@ -86,10 +86,11 @@ Table::Table(uint32_t initial, std::optional<uint32_t> maximum, Type wasmType, T
: m_maximum(maximum)
, m_type(type)
, m_wasmType(wasmType)
- , m_wasmTypeDefinition(TypeInformation::getRef(wasmType.index))
, m_isFixedSized(maximum && maximum.value() == initial)
, m_owner(nullptr)
{
+ if (auto typeDefinition = TypeInformation::getRef(wasmType.index))
+ m_typeDependencies.emplace(Ref { *typeDefinition });
setLength(initial);
ASSERT(!m_maximum || *m_maximum >= m_length);
}
diff --git a/Source/JavaScriptCore/wasm/WasmTable.h b/Source/JavaScriptCore/wasm/WasmTable.h
index bf9c237323a0..0e1494cf46aa 100644
--- a/Source/JavaScriptCore/wasm/WasmTable.h
+++ b/Source/JavaScriptCore/wasm/WasmTable.h
@@ -33,6 +33,7 @@
#include <JavaScriptCore/WasmCallee.h>
#include <JavaScriptCore/WasmFormat.h>
#include <JavaScriptCore/WasmLimits.h>
+#include <JavaScriptCore/WasmTypeDefinition.h>
#include <JavaScriptCore/WriteBarrier.h>
#include <wtf/MallocPtr.h>
#include <wtf/Ref.h>
@@ -106,8 +107,9 @@ protected:
NO_UNIQUE_ADDRESS const std::optional<uint32_t> m_maximum;
const TableElementType m_type;
Type m_wasmType;
- // If m_wasmType came from a TypeDefinition, the following retains the definition to prevent a dangling m_wasmType.
- RefPtr<const TypeDefinition> m_wasmTypeDefinition;
+ // If m_wasmType came from a TypeDefinition, the following retains the definition
+ // and all transitively reachable types to prevent dangling TypeIndex values.
+ std::optional<WebAssemblyGCTypeDependencies> m_typeDependencies;
bool m_isFixedSized { false };
JSWebAssemblyTable* m_owner;
};
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Visiting a website may leak sensitive data
Researchers: Jody Ritonga
Key insight. Clipboard access was gated on ‘is a user gesture being processed anywhere’ (UserGestureIndicator::processingUserGesture) instead of ‘does this frame’s own global object have transient activation’, letting a cross-origin iframe launder a top-level click through postMessage; transient activation, unlike a user-gesture token, does not cross the origin boundary.
Patch Details
diff --git a/LayoutTests/editing/async-clipboard/clipboard-change-data-while-getting-type.html b/LayoutTests/editing/async-clipboard/clipboard-change-data-while-getting-type.html
index 50a1cb1d546b..7087f0fcd025 100644
--- a/LayoutTests/editing/async-clipboard/clipboard-change-data-while-getting-type.html
+++ b/LayoutTests/editing/async-clipboard/clipboard-change-data-while-getting-type.html
@@ -14,7 +14,7 @@
description("This test verifies that if platform pasteboard contents are changed before reading from a ClipboardItem, the new contents of the pasteboard should not be exposed to the page and the result of getType() should be rejected. This test requires WebKitTestRunner.");
await UIHelper.copyText("Foo");
- items = await navigator.clipboard.read();
+ items = await readClipboardWithUserActivation();
shouldBe("items.length", "1");
await UIHelper.copyText("Bar");
try {
diff --git a/LayoutTests/editing/async-clipboard/clipboard-get-type-with-old-items.html b/LayoutTests/editing/async-clipboard/clipboard-get-type-with-old-items.html
index 2e04b9df561a..7e2b7c466aba 100644
--- a/LayoutTests/editing/async-clipboard/clipboard-get-type-with-old-items.html
+++ b/LayoutTests/editing/async-clipboard/clipboard-get-type-with-old-items.html
@@ -14,7 +14,7 @@
description("This test verifies that it's still possible to read from the clipboard, after attempting to read data from stale items. Requires DumpRenderTree or WebKitTestRunner.");
await UIHelper.copyText("Hello");
- itemsBeforeChange = await navigator.clipboard.read();
+ itemsBeforeChange = await readClipboardWithUserActivation();
shouldBe("itemsBeforeChange.length", "1");
try {
@@ -26,7 +26,7 @@
}
await UIHelper.copyText("World");
- itemsAfterChange = await navigator.clipboard.read();
+ itemsAfterChange = await readClipboardWithUserActivation();
shouldBe("itemsAfterChange.length", "1");
try {
diff --git a/LayoutTests/editing/async-clipboard/resources/async-clipboard-helpers.js b/LayoutTests/editing/async-clipboard/resources/async-clipboard-helpers.js
index 9d00786fa29d..ec3a98544330 100644
--- a/LayoutTests/editing/async-clipboard/resources/async-clipboard-helpers.js
+++ b/LayoutTests/editing/async-clipboard/resources/async-clipboard-helpers.js
@@ -99,3 +99,29 @@ async function checkClipboardItemString(item, type, expectedString)
else
testFailed(`getType("${type}") resolved to "${observedString}; expected "${expectedString}"`);
}
+
+// Calls navigator.clipboard.read() with a fresh user activation, as required by the W3C
+// Clipboard API spec. Adds a transient button to the document, activates it, and reads the
+// clipboard while the relevant global object has transient activation.
+async function readClipboardWithUserActivation()
+{
+ const button = document.createElement("button");
+ button.textContent = "Read clipboard";
+ document.body.appendChild(button);
+
+ // Wait for the freshly-added button to be committed to the UI process's layer tree
+ // before tapping it. Otherwise, on iOS the tap gesture may hit-test before the render
+ // tree update is committed, miss the button, and cause the test to time out waiting for
+ // the click that never arrives.
+ await UIHelper.ensurePresentationUpdate();
+
+ const clicked = new Promise(resolve => button.addEventListener("click", resolve, { once: true }));
+ UIHelper.activateElement(button);
+ await clicked;
+
+ try {
+ return await navigator.clipboard.read();
+ } finally {
+ button.remove();
+ }
+}
diff --git a/LayoutTests/http/tests/security/clipboard/clipboard-access-in-cross-origin-iframe-denied-expected.txt b/LayoutTests/http/tests/security/clipboard/clipboard-access-in-cross-origin-iframe-denied-expected.txt
new file mode 100644
index 000000000000..4e78265745ad
--- /dev/null
+++ b/LayoutTests/http/tests/security/clipboard/clipboard-access-in-cross-origin-iframe-denied-expected.txt
@@ -0,0 +1,21 @@
+Verifies that a cross-origin iframe cannot read or write the clipboard by consuming transient user activation from the top-level page via postMessage. navigator.clipboard.readText(), navigator.clipboard.read(), navigator.clipboard.writeText(), and navigator.clipboard.write() in the iframe should all be rejected with NotAllowedError because transient activation does not propagate to cross-origin iframes.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+Result for navigator.clipboard.readText():
+PASS iframeStatus is "rejected"
+PASS iframeErrorName is "NotAllowedError"
+Result for navigator.clipboard.read():
+PASS iframeStatus is "rejected"
+PASS iframeErrorName is "NotAllowedError"
+Result for navigator.clipboard.writeText():
+PASS iframeStatus is "rejected"
+PASS iframeErrorName is "NotAllowedError"
+Result for navigator.clipboard.write():
+PASS iframeStatus is "rejected"
+PASS iframeErrorName is "NotAllowedError"
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/http/tests/security/clipboard/clipboard-access-in-cross-origin-iframe-denied.html b/LayoutTests/http/tests/security/clipboard/clipboard-access-in-cross-origin-iframe-denied.html
new file mode 100644
index 000000000000..7e92f2069b9a
--- /dev/null
+++ b/LayoutTests/http/tests/security/clipboard/clipboard-access-in-cross-origin-iframe-denied.html
@@ -0,0 +1,73 @@
+<!DOCTYPE html> <!-- webkit-test-runner [ AsyncClipboardAPIEnabled=true JavaScriptCanAccessClipboard=false ] -->
+<html>
+<head>
+<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
+<script src="/resources/js-test-pre.js"></script>
+<script src="/js-test-resources/ui-helper.js"></script>
+<style>
+button, iframe {
+ display: block;
+ width: 400px;
+ height: 100px;
+}
+</style>
+</head>
+<body>
+<p id="description"></p>
+<div id="console"></div>
+<button id="run">Click to postMessage clipboard request to cross-origin iframe</button>
+<iframe id="frame" src="http://localhost:8080/security/clipboard/resources/clipboard-access-from-iframe.html"></iframe>
+<script>
+description("Verifies that a cross-origin iframe cannot read or write the clipboard by consuming transient user activation from the top-level page via postMessage. navigator.clipboard.readText(), navigator.clipboard.read(), navigator.clipboard.writeText(), and navigator.clipboard.write() in the iframe should all be rejected with NotAllowedError because transient activation does not propagate to cross-origin iframes.");
+jsTestIsAsync = true;
+
+const frame = document.getElementById("frame");
+const runButton = document.getElementById("run");
+
+let pendingMethod = null;
+let pendingResolve = null;
+
+runButton.addEventListener("click", () => {
+ frame.contentWindow.postMessage({ method: pendingMethod }, "*");
+});
+
+async function testMethod(method) {
+ pendingMethod = method;
+ const result = await new Promise(resolve => {
+ pendingResolve = resolve;
+ UIHelper.activateElement(runButton);
+ });
+ window.iframeStatus = result.status;
+ window.iframeErrorName = result.errorName;
+ debug(`Result for navigator.clipboard.${method}():`);
+ shouldBeEqualToString("iframeStatus", "rejected");
+ shouldBeEqualToString("iframeErrorName", "NotAllowedError");
+}
+
+window.addEventListener("message", async event => {
+ if (!event.data)
+ return;
+
+ if (event.data.type === "iframe-loaded") {
+ if (!window.testRunner)
+ return;
+ await testMethod("readText");
+ await testMethod("read");
+ await testMethod("writeText");
+ await testMethod("write");
+ runButton.remove();
+ frame.remove();
+ finishJSTest();
+ return;
+ }
+
+ if (event.data.type === "iframe-result" && pendingResolve) {
+ const resolve = pendingResolve;
+ pendingResolve = null;
+ resolve(event.data);
+ }
+});
+</script>
+<script src="/resources/js-test-post.js"></script>
+</body>
+</html>
diff --git a/LayoutTests/http/tests/security/clipboard/resources/clipboard-access-from-iframe.html b/LayoutTests/http/tests/security/clipboard/resources/clipboard-access-from-iframe.html
new file mode 100644
index 000000000000..ab7be18f5f2f
--- /dev/null
+++ b/LayoutTests/http/tests/security/clipboard/resources/clipboard-access-from-iframe.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<html>
+<body>
+<script>
+async function invoke(method) {
+ switch (method) {
+ case "readText":
+ return navigator.clipboard.readText();
+ case "read":
+ return navigator.clipboard.read();
+ case "writeText":
+ return navigator.clipboard.writeText("iframe-written text");
+ case "write": {
+ const blob = new Blob(["iframe-written text"], { type: "text/plain" });
+ return navigator.clipboard.write([new ClipboardItem({ "text/plain": blob })]);
+ }
+ }
+ throw new Error(`unknown method: ${method}`);
+}
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to memory corruption
Researchers: Milad Nasr and Nicholas Carlini with Claude, Anthropic
Key insight. Snapshotting clients into a Ref vector kept them alive but not registered; a client removed mid-iteration was still invoked. Re-checking set membership (clients.contains) before each callback is the fix.
Patch Details
diff --git a/LayoutTests/fonts/font-face-load-crash-expected.txt b/LayoutTests/fonts/font-face-load-crash-expected.txt
new file mode 100644
index 000000000000..b0352cadbf49
--- /dev/null
+++ b/LayoutTests/fonts/font-face-load-crash-expected.txt
@@ -0,0 +1 @@
+This test passes if WebKit does not crash.
diff --git a/LayoutTests/fonts/font-face-load-crash.html b/LayoutTests/fonts/font-face-load-crash.html
new file mode 100644
index 000000000000..43472c0fb8fc
--- /dev/null
+++ b/LayoutTests/fonts/font-face-load-crash.html
@@ -0,0 +1,17 @@
+<style id="v">x{}</style>
+<body>
+<script>
+window?.testRunner?.dumpAsText();
+let s = document.createElement('style');
+s.textContent = '@font-face { font-family: t; src: local(Helvetica); }';
+document.head.appendChild(s);
+document.body.offsetHeight;
+let face = [...document.fonts].find(f => f.family.replace(/['"]/g, '') === 't');
+face.loaded;
+Object.defineProperty(FontFace.prototype, 'then', { get() {
+ document.getElementById('v').remove();
+ document.body.offsetHeight;
+}, configurable: true });
+face.load();
+document.write('This test passes if WebKit does not crash.');
+</script>
diff --git a/Source/WebCore/css/CSSFontFace.cpp b/Source/WebCore/css/CSSFontFace.cpp
index ac6f2b015f8a..6515d3514d9a 100644
--- a/Source/WebCore/css/CSSFontFace.cpp
+++ b/Source/WebCore/css/CSSFontFace.cpp
@@ -62,8 +62,10 @@ DEFINE_ALLOCATOR_WITH_HEAP_IDENTIFIER(CSSFontFace);
static void iterateClients(WeakHashSet<CSSFontFaceClient>& clients, NOESCAPE const Function<void(CSSFontFaceClient&)>& callback)
{
- for (auto& client : copyToVectorOf<Ref<CSSFontFaceClient>>(clients))
- callback(client);
+ for (auto& client : copyToVectorOf<Ref<CSSFontFaceClient>>(clients)) {
+ if (clients.contains(client))
+ callback(client);
+ }
}
void CSSFontFace::appendSources(CSSFontFace& fontFace, CSSValueList& srcList, ScriptExecutionContext* context, bool isInitiatingElementInUserAgentShadowTree)
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: Maher Azzouzi, Tuan and Duc from Calif.io, OpenAI Codex Security - Amy Burnett, Evan Lambert
Key insight. The ArrayBuffer<->WebAssembly.Memory association was stored on the non-GC native buffer and thus invisible to the collector, so it could dangle across GC; moving it to a write-barriered JS wrapper (and forbidding native resize of wasm memory) makes the link GC-safe.
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: Nan Wang (@eternalsakura13)
Background
Advisory: Apple Security Advisory
| Attribute | Value |
|---|---|
| CVE | CVE-2026-43717 |
| Bugzilla | 313351 |
| Component | WebKit |
| Bug Class | UAF |
| Severity | medium |
| Advisory | Apple Advisory |
Fix not public. The WebKit fix for this bug is embargoed or not yet disclosed. The bug ID is confirmed from the Apple advisory, but no public commit references it.
Root Cause Analysis
A use-after-free issue was addressed with improved memory management.
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: Nan Wang (@eternalsakura13)
Background
Advisory: Apple Security Advisory
| Attribute | Value |
|---|---|
| CVE | CVE-2026-43718 |
| Bugzilla | 313350 |
| Component | WebKit |
| Bug Class | IntegerOverflow |
| Severity | medium |
| Advisory | Apple Advisory |
Fix not public. The WebKit fix for this bug is embargoed or not yet disclosed. The bug ID is confirmed from the Apple advisory, but no public commit references it.
Root Cause Analysis
A stack overflow was addressed with improved input validation.
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: Josef Korbel, Gia Bui (@yabeow) from Calif.io
Key insight. getContext cached m_context’s nullness across a WebIDL dictionary conversion that can run script and re-entrantly set it, then created a second context and clobbered the first — re-checking m_context (and detach state) after every script-running step closes it.
Patch Details
diff --git a/LayoutTests/fast/canvas/canvas-getContext-reentrant-expected.txt b/LayoutTests/fast/canvas/canvas-getContext-reentrant-expected.txt
new file mode 100644
index 000000000000..d801ccb9fae7
--- /dev/null
+++ b/LayoutTests/fast/canvas/canvas-getContext-reentrant-expected.txt
@@ -0,0 +1,33 @@
+getContext("2d") with reentrant getContext("2d"): outer=CanvasRenderingContext2D inner=CanvasRenderingContext2D
+getContext("2d") with reentrant getContext("bitmaprenderer"): outer=null inner=ImageBitmapRenderingContext
+getContext("2d") with reentrant getContext("webgl"): outer=null inner=WebGLRenderingContext
+getContext("2d") with reentrant getContext("webgl2"): outer=null inner=WebGL2RenderingContext
+getContext("bitmaprenderer") with reentrant getContext("2d"): outer=null inner=CanvasRenderingContext2D
+getContext("bitmaprenderer") with reentrant getContext("bitmaprenderer"): outer=ImageBitmapRenderingContext inner=ImageBitmapRenderingContext
+getContext("bitmaprenderer") with reentrant getContext("webgl"): outer=null inner=WebGLRenderingContext
+getContext("bitmaprenderer") with reentrant getContext("webgl2"): outer=null inner=WebGL2RenderingContext
+getContext("webgl") with reentrant getContext("2d"): outer=null inner=CanvasRenderingContext2D
+getContext("webgl") with reentrant getContext("bitmaprenderer"): outer=null inner=ImageBitmapRenderingContext
+getContext("webgl") with reentrant getContext("webgl"): outer=WebGLRenderingContext inner=WebGLRenderingContext
+getContext("webgl") with reentrant getContext("webgl2"): outer=null inner=WebGL2RenderingContext
+getContext("webgl2") with reentrant getContext("2d"): outer=null inner=CanvasRenderingContext2D
+getContext("webgl2") with reentrant getContext("bitmaprenderer"): outer=null inner=ImageBitmapRenderingContext
+getContext("webgl2") with reentrant getContext("webgl"): outer=null inner=WebGLRenderingContext
+getContext("webgl2") with reentrant getContext("webgl2"): outer=WebGL2RenderingContext inner=WebGL2RenderingContext
+OffscreenCanvas getContext("2d") with reentrant getContext("2d"): outer=OffscreenCanvasRenderingContext2D inner=OffscreenCanvasRenderingContext2D
+OffscreenCanvas getContext("2d") with reentrant getContext("bitmaprenderer"): outer=null inner=ImageBitmapRenderingContext
+OffscreenCanvas getContext("2d") with reentrant getContext("webgl"): outer=null inner=WebGLRenderingContext
+OffscreenCanvas getContext("2d") with reentrant getContext("webgl2"): outer=null inner=WebGL2RenderingContext
+OffscreenCanvas getContext("bitmaprenderer") with reentrant getContext("2d"): outer=null inner=OffscreenCanvasRenderingContext2D
+OffscreenCanvas getContext("bitmaprenderer") with reentrant getContext("bitmaprenderer"): outer=ImageBitmapRenderingContext inner=ImageBitmapRenderingContext
+OffscreenCanvas getContext("bitmaprenderer") with reentrant getContext("webgl"): outer=null inner=WebGLRenderingContext
+OffscreenCanvas getContext("bitmaprenderer") with reentrant getContext("webgl2"): outer=null inner=WebGL2RenderingContext
+OffscreenCanvas getContext("webgl") with reentrant getContext("2d"): outer=null inner=OffscreenCanvasRenderingContext2D
+OffscreenCanvas getContext("webgl") with reentrant getContext("bitmaprenderer"): outer=null inner=ImageBitmapRenderingContext
+OffscreenCanvas getContext("webgl") with reentrant getContext("webgl"): outer=WebGLRenderingContext inner=WebGLRenderingContext
+OffscreenCanvas getContext("webgl") with reentrant getContext("webgl2"): outer=null inner=WebGL2RenderingContext
+OffscreenCanvas getContext("webgl2") with reentrant getContext("2d"): outer=null inner=OffscreenCanvasRenderingContext2D
+OffscreenCanvas getContext("webgl2") with reentrant getContext("bitmaprenderer"): outer=null inner=ImageBitmapRenderingContext
+OffscreenCanvas getContext("webgl2") with reentrant getContext("webgl"): outer=null inner=WebGLRenderingContext
+OffscreenCanvas getContext("webgl2") with reentrant getContext("webgl2"): outer=WebGL2RenderingContext inner=WebGL2RenderingContext
+PASS if no crash.
diff --git a/LayoutTests/fast/canvas/canvas-getContext-reentrant.html b/LayoutTests/fast/canvas/canvas-getContext-reentrant.html
new file mode 100644
index 000000000000..e46e9d41f37d
--- /dev/null
+++ b/LayoutTests/fast/canvas/canvas-getContext-reentrant.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<script>
+if (window.testRunner)
+ testRunner.dumpAsText();
+
+function gc() {
+ if (window.GCController)
+ return GCController.collect();
+ for (let i = 0; i < 10000; i++)
+ new String("abc");
+}
+
+function reenter(outer, inner) {
+ let canvas = document.createElement("canvas");
+ let innerCtx;
+ let outerCtx = canvas.getContext(outer, {
+ get colorSpace() { innerCtx = canvas.getContext(inner); return "srgb"; },
+ get alpha() { innerCtx = canvas.getContext(inner); return true; },
+ });
+ document.write(`getContext("${outer}") with reentrant getContext("${inner}"): outer=${name(outerCtx)} inner=${name(innerCtx)}<br>`);
+}
+
+function reenterOffscreen(outer, inner) {
+ let canvas = new OffscreenCanvas(10, 10);
+ let innerCtx;
+ let outerCtx = canvas.getContext(outer, {
+ get colorSpace() { innerCtx = canvas.getContext(inner); return "srgb"; },
+ get alpha() { innerCtx = canvas.getContext(inner); return true; },
+ });
+ document.write(`OffscreenCanvas getContext("${outer}") with reentrant getContext("${inner}"): outer=${name(outerCtx)} inner=${name(innerCtx)}<br>`);
+}
+
+function name(ctx) {
+ return ctx ? ctx.constructor.name : "null";
+}
+
+for (let outer of ["2d", "bitmaprenderer", "webgl", "webgl2"])
+ for (let inner of ["2d", "bitmaprenderer", "webgl", "webgl2"])
+ reenter(outer, inner);
+
+for (let outer of ["2d", "bitmaprenderer", "webgl", "webgl2"])
+ for (let inner of ["2d", "bitmaprenderer", "webgl", "webgl2"])
+ reenterOffscreen(outer, inner);
+
+gc();
+document.write("PASS if no crash.");
+</script>
diff --git a/Source/WebCore/html/HTMLCanvasElement.cpp b/Source/WebCore/html/HTMLCanvasElement.cpp
index c280824242a4..2bc4b33962b8 100644
--- a/Source/WebCore/html/HTMLCanvasElement.cpp
+++ b/Source/WebCore/html/HTMLCanvasElement.cpp
@@ -226,7 +226,8 @@ void HTMLCanvasElement::setSizeForControllingContext(IntSize newSize)
ExceptionOr<std::optional<RenderingContext>> HTMLCanvasElement::getContext(JSC::JSGlobalObject& state, const String& contextId, FixedVector<JSC::Strong<JSC::Unknown>>&& arguments)
{
- if (m_context) {
+ auto getExistingContext = [&]() -> ExceptionOr<std::optional<RenderingContext>> {
+ ASSERT(m_context);
if (m_context->isPlaceholder())
return Exception { ExceptionCode::InvalidStateError };
@@ -263,7 +264,10 @@ ExceptionOr<std::optional<RenderingContext>> HTMLCanvasElement::getContext(JSC::
ASSERT_NOT_REACHED();
return std::optional<RenderingContext> { std::nullopt };
- }
+ };
+
+ if (m_context)
+ return getExistingContext();
if (is2dType(contextId)) {
Ref vm = state.vm();
@@ -273,6 +277,10 @@ ExceptionOr<std::optional<RenderingContext>> HTMLCanvasElement::getContext(JSC::
if (settings.hasException(scope)) [[unlikely]]
return Exception { ExceptionCode::ExistingExceptionError };
+ // Dictionary conversion may run script, which may have set m_context via a re-entrant getContext().
+ if (m_context) [[unlikely]]
+ return getExistingContext();
+
RefPtr context = createContext2d(contextId, settings.releaseReturnValue());
if (!context)
return std::optional<RenderingContext> { std::nullopt };
@@ -287,6 +295,10 @@ ExceptionOr<std::optional<RenderingContext>> HTMLCanvasElement::getContext(JSC::
if (settings.hasException(scope)) [[unlikely]]
return Exception { ExceptionCode::ExistingExceptionError };
+ // Dictionary conversion may run script, which may have set m_context via a re-entrant getContext().
+ if (m_context) [[unlikely]]
+ return getExistingContext();
+
RefPtr context = createContextBitmapRenderer(contextId, settings.releaseReturnValue());
if (!context)
return std::optional<RenderingContext> { std::nullopt };
@@ -302,6 +314,10 @@ ExceptionOr<std::optional<RenderingContext>> HTMLCanvasElement::getContext(JSC::
if (attributes.hasException(scope)) [[unlikely]]
return Exception { ExceptionCode::ExistingExceptionError };
+ // Dictionary conversion may run script, which may have set m_context via a re-entrant getContext().
+ if (m_context) [[unlikely]]
+ return getExistingContext();
+
RefPtr context = createContextWebGL(toWebGLVersion(contextId), attributes.releaseReturnValue());
if (!context)
return std::optional<RenderingContext> { std::nullopt };
@@ -355,7 +371,7 @@ bool HTMLCanvasElement::is2dType(const String& type)
CanvasRenderingContext2D* HTMLCanvasElement::createContext2d(const String& type, CanvasRenderingContext2DSettings&& settings)
{
ASSERT_UNUSED(HTMLCanvasElement::is2dType(type), type);
- ASSERT(!m_context);
+ ASSERT_WITH_SECURITY_IMPLICATION(!m_context);
m_context = CanvasRenderingContext2D::create(*this, WTF::move(settings), document().inQuirksMode());
if (!m_context)
@@ -403,7 +419,7 @@ WebGLVersion HTMLCanvasElement::toWebGLVersion(const String& type)
WebGLRenderingContextBase* HTMLCanvasElement::createContextWebGL(WebGLVersion type, WebGLContextAttributes&& attrs)
{
- ASSERT(!m_context);
+ ASSERT_WITH_SECURITY_IMPLICATION(!m_context);
if (!document().settings().webGLEnabled())
return nullptr;
@@ -459,7 +475,7 @@ bool HTMLCanvasElement::isBitmapRendererType(const String& type)
ImageBitmapRenderingContext* HTMLCanvasElement::createContextBitmapRenderer(const String& type, ImageBitmapRenderingContextSettings&& settings)
{
ASSERT_UNUSED(type, HTMLCanvasElement::isBitmapRendererType(type));
- ASSERT(!m_context);
+ ASSERT_WITH_SECURITY_IMPLICATION(!m_context);
auto context = ImageBitmapRenderingContext::create(*this, WTF::move(settings));
WeakPtr weakContext = *context;
@@ -491,7 +507,7 @@ bool HTMLCanvasElement::isWebGPUType(const String& type)
GPUCanvasContext* HTMLCanvasElement::createContextWebGPU(const String& type, GPU* gpu)
{
ASSERT_UNUSED(type, HTMLCanvasElement::isWebGPUType(type));
- ASSERT(!m_context);
+ ASSERT_WITH_SECURITY_IMPLICATION(!m_context);
if (!document().settings().webGPUEnabled() || !gpu)
return nullptr;
diff --git a/Source/WebCore/html/OffscreenCanvas.cpp b/Source/WebCore/html/OffscreenCanvas.cpp
index f67caa13c15d..5adbd49dc2d5 100644
--- a/Source/WebCore/html/OffscreenCanvas.cpp
+++ b/Source/WebCore/html/OffscreenCanvas.cpp
@@ -183,6 +183,13 @@ ExceptionOr<std::optional<OffscreenRenderingContext>> OffscreenCanvas::getContex
if (m_detached)
return Exception { ExceptionCode::InvalidStateError };
+ // Dictionary conversion may run script, which may have detached this canvas.
+ auto shouldThrowForDetachedCanvas = [&]() -> ExceptionOr<void> {
+ if (m_detached) [[unlikely]]
+ return Exception { ExceptionCode::InvalidStateError };
+ return { };
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: A malicious website may be able to silently hijack clipboard data
Researchers: Idan Masas
Key insight. Preserving transient user activation was conditioned on the wrong signal — ’no prompt shown’ — instead of ’the caller actually had a gesture’, so a no-gesture, no-prompt rejection leaked activation the frame never earned; the fix restores the invariant by requiring the gesture state captured at call time.
Patch Details
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 @@
+<!DOCTYPE html><!-- webkit-test-runner [ JavaScriptCanOpenWindowsAutomatically=false ] -->
+<html>
+<head>
+ <script src="/js-test-resources/js-test.js"></script>
+ <script src="/js-test-resources/ui-helper.js"></script>
+ <script src="/resourceLoadStatistics/resources/util.js"></script>
+ <script>
+ description("Tests that rejecting requestStorageAccess() without a user gesture does not synthesize user activation.");
+ jsTestIsAsync = true;
+
+ const hostUnderTest = "localhost:8000";
+ const statisticsUrl = "http://" + hostUnderTest + "/temp";
+
+ window.addEventListener("message", receiveMessage, false);
+
+ function receiveMessage(event) {
+ if (event.origin === "http://localhost:8000") {
+ if (event.data.indexOf("PASS ") !== -1)
+ testPassed(event.data.replace("PASS ", ""));
+ else
+ testFailed(event.data.replace("FAIL ", ""));
+ } else
+ testFailed("Received a message from an unexpected origin: " + event.origin);
+ setEnableFeature(false, finishJSTest);
+ }
+
+ function runTest() {
+ setEnableFeature(true, function() {
+ testRunner.setStatisticsPrevalentResource(statisticsUrl, true, function() {
+ if (!testRunner.isStatisticsPrevalentResource(statisticsUrl))
+ testFailed("Host did not get set as prevalent resource.");
+ testRunner.setStatisticsHasHadUserInteraction(statisticsUrl, false, function() {
+ if (testRunner.isStatisticsHasHadUserInteraction(statisticsUrl))
+ testFailed("Host did get logged for user interaction.");
+ testRunner.statisticsUpdateCookieBlocking(function() {
+ let iframeElement = document.createElement("iframe");
+ iframeElement.id = "TheIframeThatRequestsStorageAccess";
+ iframeElement.src = "http://localhost:8000/storageAccess/resources/request-storage-access-without-gesture-check-activation-iframe.html";
+ document.body.appendChild(iframeElement);
+ });
+ });
+ });
+ });
+ }
+ </script>
+</head>
+<body onload="runTest()">
+</body>
+</html>
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 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script>
+ function makeRequestWithoutUserGesture() {
+ document.requestStorageAccess().then(
+ function () {
+ top.postMessage("FAIL requestStorageAccess() was granted without user gesture.", "http://127.0.0.1:8000");
+ },
+ function () {
+ let win = window.open("about:blank", "_blank");
+ if (win) {
+ win.close();
+ top.postMessage("FAIL Rejection handler had user activation despite no user gesture.", "http://127.0.0.1:8000");
+ } else
+ top.postMessage("PASS Rejection handler correctly had no user activation.", "http://127.0.0.1:8000");
+ }
+ );
+ }
+ </script>
+</head>
+<body onload="makeRequestWithoutUserGesture()">
+</body>
+</html>
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<DeferredPromise>&& 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<DeferredPromise>&& 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 @@ public:
virtual RefPtr<Icon> createIconForFiles(const Vector<String>& /* filenames */) = 0;
virtual void hasStorageAccess(RegistrableDomain&& /*subFrameDomain*/, RegistrableDomain&& /*topFrameDomain*/, LocalFrame&, CompletionHandler<void(bool)>&& completionHandler) { completionHandler(false); }
- virtual void requestStorageAccess(RegistrableDomain&& subFrameDomain, RegistrableDomain&& topFrameDomain, LocalFrame&, StorageAccessScope scope, HasOrShouldIgnoreUserGesture, CompletionHandler<void(RequestStorageAccessResult)>&& 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<void(RequestStorageAccessResult)>&& 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<void()>&&) { }
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<void(RequestStorageAccessResult)>&& completionHandler)
+void WebResourceLoadStatisticsStore::requestStorageAccess(RegistrableDomain&& subFrameDomain, RegistrableDomain&& topFrameDomain, FrameIdentifier frameID, PageIdentifier webPageID, WebPageProxyIdentifier webPageProxyID, StorageAccessScope scope, HasUserGestureOrNoUserGestureRequired hasUserGestureOrNoUserGestureRequired, CompletionHandler<void(RequestStorageAccessResult)>&& 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 @@ public:
void hasHadUserInteraction(RegistrableDomain&&, CompletionHandler<void(bool)>&&);
void hasStorageAccess(SubFrameDomain&&, TopFrameDomain&&, std::optional<WebCore::FrameIdentifier>, WebCore::PageIdentifier, CompletionHandler<void(bool)>&&);
bool hasStorageAccessForFrame(const SubFrameDomain&, const TopFrameDomain&, WebCore::FrameIdentifier, WebCore::PageIdentifier);
- void requestStorageAccess(SubFrameDomain&&, TopFrameDomain&&, WebCore::FrameIdentifier, WebCore::PageIdentifier, WebPageProxyIdentifier, StorageAccessScope, WebCore::HasOrShouldIgnoreUserGesture, CompletionHandler<void(RequestStorageAccessResult)>&&);
+ void requestStorageAccess(SubFrameDomain&&, TopFrameDomain&&, WebCore::FrameIdentifier, WebCore::PageIdentifier, WebPageProxyIdentifier, StorageAccessScope, WebCore::HasUserGestureOrNoUserGestureRequired, CompletionHandler<void(RequestStorageAccessResult)>&&);
void queryStorageAccessPermission(SubFrameDomain&&, TopFrameDomain&&, std::optional<WebPageProxyIdentifier>, CompletionHandler<void(WebCore::PermissionState)>&&);
void setLoginStatus(RegistrableDomain&&, IsLoggedIn, std::optional<LoginStatus>&&, CompletionHandler<void()>&&);
void isLoggedIn(RegistrableDomain&&, CompletionHandler<void(bool)>&&);
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
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: A malicious website may be able to process restricted web content outside the sandbox
Researchers: Luke Francis
Key insight. A renderer-reachable image load validated only that the URL was well-formed, not that it was HTTP(S), so a compromised WebContent process could coerce the Network process into reading local file:// paths; restricting to protocolIsInHTTPFamily() and enforcing cookie policy closes it.
Patch Details
diff --git a/LayoutTests/ipc/load-image-for-decoding-file-url-expected.txt b/LayoutTests/ipc/load-image-for-decoding-file-url-expected.txt
new file mode 100644
index 000000000000..4732d73458a8
--- /dev/null
+++ b/LayoutTests/ipc/load-image-for-decoding-file-url-expected.txt
@@ -0,0 +1,2 @@
+PASS: file:// rejected by MESSAGE_CHECK
+
diff --git a/LayoutTests/ipc/load-image-for-decoding-file-url.html b/LayoutTests/ipc/load-image-for-decoding-file-url.html
new file mode 100644
index 000000000000..c4b04a09abf7
--- /dev/null
+++ b/LayoutTests/ipc/load-image-for-decoding-file-url.html
@@ -0,0 +1,68 @@
+<!DOCTYPE html><!-- webkit-test-runner [ IPCTestingAPIEnabled=true ] -->
+<html><body>
+<pre id="log"></pre>
+<script>
+if (window.testRunner) {
+ testRunner.dumpAsText();
+ testRunner.waitUntilDone();
+}
+function log(s) { document.getElementById('log').textContent += s + '\n'; }
+
+async function main() {
+ if (!window.IPC) {
+ log('PASS: file:// rejected by MESSAGE_CHECK');
+ return;
+ }
+
+ const { CoreIPC, ArgumentSerializer } = await import('./coreipc.js');
+
+ const fileRequest = {
+ getRequestDataToSerialize: {
+ variantType: 'WebCore::ResourceRequest::RequestData',
+ variant: {
+ m_url: { string: 'file:///private/etc/hosts' },
+ m_firstPartyForCookies: { string: 'file:///private/etc/hosts' },
+ m_timeoutInterval: 60.0,
+ m_httpMethod: 'GET',
+ m_httpHeaderFields: { commonHeaders: [], uncommonHeaders: [] },
+ m_responseContentDispositionEncodingFallbackArray: [],
+ m_cachePolicy: 0,
+ m_sameSiteDisposition: 1,
+ m_priority: 2,
+ m_requester: 0,
+ m_allowCookies: true,
+ m_isTopSite: true,
+ m_isAppInitiated: true,
+ m_privacyProxyFailClosedForUnreachableNonMainHosts: false,
+ m_useAdvancedPrivacyProtections: false,
+ m_didFilterLinkDecoration: false,
+ m_isPrivateTokenUsageByThirdPartyAllowed: false,
+ m_wasSchemeOptimisticallyUpgraded: false,
+ m_targetAddressSpace: 0,
+ },
+ },
+ cachePartition: '',
+ hiddenFromInspector: false,
+ };
+
+ const definition = IPC.messages.NetworkConnectionToWebProcess_LoadImageForDecoding;
+ const args = ArgumentSerializer.serializeArguments(definition.arguments,
+ { request: fileRequest, pageID: BigInt(IPC.webPageProxyID), maximumBytesFromNetwork: 1024n });
+
+ CoreIPC.Networking.NetworkConnectionToWebProcess.TakeInvalidMessageStringForTesting(0, {}, () => { });
+
+ IPC.connectionForProcessTarget('Networking').sendWithAsyncReply(0, definition.name, args, () => { });
+
+ let messageCheck = null;
+ CoreIPC.Networking.NetworkConnectionToWebProcess.TakeInvalidMessageStringForTesting(0, {},
+ reply => messageCheck = reply.error);
+
+ if (messageCheck && messageCheck.includes('Message check failed') && messageCheck.includes('protocolIsInHTTPFamily'))
+ log('PASS: file:// rejected by MESSAGE_CHECK');
+ else
+ log('FAIL: expected MESSAGE_CHECK on protocolIsInHTTPFamily, got: "' + messageCheck + '"');
+}
+
+main().catch(e => log('FAIL: ' + e + '\n' + (e.stack || ''))).finally(() => window.testRunner?.notifyDone());
+</script>
+</body></html>
diff --git a/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp b/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp
index 407bc78dae17..9b5a76d15a83 100644
--- a/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp
+++ b/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp
@@ -241,7 +241,8 @@ void NetworkConnectionToWebProcess::hasUploadStateChanged(bool hasUpload)
void NetworkConnectionToWebProcess::loadImageForDecoding(WebCore::ResourceRequest&& request, WebPageProxyIdentifier pageID, uint64_t maximumBytesFromNetwork, CompletionHandler<void(Expected<Ref<WebCore::FragmentedSharedBuffer>, WebCore::ResourceError>&&)>&& completionHandler)
{
auto url = request.url();
- MESSAGE_CHECK_COMPLETION(url.isValid(), completionHandler(makeUnexpected<WebCore::ResourceError>({ })));
+ MESSAGE_CHECK_COMPLETION(url.isValid() && url.protocolIsInHTTPFamily(), completionHandler(makeUnexpected<WebCore::ResourceError>({ })));
+ MESSAGE_CHECK_COMPLETION(m_networkProcess->allowsFirstPartyForCookies(m_webProcessIdentifier, request.firstPartyForCookies()) == NetworkProcess::AllowCookieAccess::Allow, completionHandler(makeUnexpected<WebCore::ResourceError>({ })));
CheckedPtr networkSession = this->networkSession();
if (!networkSession)
return completionHandler(makeUnexpected<WebCore::ResourceError>({ }));
diff --git a/Source/WebKit/UIProcess/WebPageProxy.cpp b/Source/WebKit/UIProcess/WebPageProxy.cpp
index 3aa1e00aa724..2ada3cb73b6e 100644
--- a/Source/WebKit/UIProcess/WebPageProxy.cpp
+++ b/Source/WebKit/UIProcess/WebPageProxy.cpp
@@ -11542,7 +11542,7 @@ void WebPageProxy::dataTaskWithRequest(WebCore::ResourceRequest&& request, const
void WebPageProxy::loadAndDecodeImage(WebCore::ResourceRequest&& request, std::optional<WebCore::FloatSize> sizeConstraint, size_t maximumBytesFromNetwork, CompletionHandler<void(Expected<Ref<WebCore::ShareableBitmap>, WebCore::ResourceError>&&)>&& completionHandler)
{
- if (isClosed() || !request.url().isValid())
+ if (isClosed() || !request.url().isValid() || !request.url().protocolIsInHTTPFamily())
return completionHandler(makeUnexpected(decodeError(request.url())));
if (!hasRunningProcess())
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: Utkarsh Pal, Tristan Madani (@TristanInSec) from Talence Security, stratan (@5tratan) of Almamater Technologies, Narendra Singh (@_3P1C), Josef Korbel (Citadelo), Gia Bui (@yabeow) from Calif.io
Key insight. renderFromBuffer indexed the buffer’s channel data without guarding an empty buffer (unsigned bufferLength-1 underflow) or an out-of-range readIndex, and setBufferForBindings only acquired content when playing — adding the bounds guards and always acquiring content fixes the OOB/UAF.
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: Tommy DeVoss from Braze Security Team (@thedawgyg), Gurpreet Shergill, Gia Bui (@yabeow) from Calif.io
Key insight. Invoking completion handlers while the loaders were still owned by the member vector let a re-entrant call free or mutate that vector mid-iteration; moving it out first with std::exchange closes the re-entrancy.
Patch Details
diff --git a/LayoutTests/editing/async-clipboard/clipboard-write-item-crash-expected.txt b/LayoutTests/editing/async-clipboard/clipboard-write-item-crash-expected.txt
new file mode 100644
index 000000000000..d1f6de04a520
--- /dev/null
+++ b/LayoutTests/editing/async-clipboard/clipboard-write-item-crash-expected.txt
@@ -0,0 +1,3 @@
+This test passes if WebKit does not hit assertions or crash under ASAN.
+
+PASS
diff --git a/LayoutTests/editing/async-clipboard/clipboard-write-item-crash.html b/LayoutTests/editing/async-clipboard/clipboard-write-item-crash.html
new file mode 100644
index 000000000000..aaa046f74ab3
--- /dev/null
+++ b/LayoutTests/editing/async-clipboard/clipboard-write-item-crash.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html><!-- webkit-test-runner [ AsyncClipboardAPIEnabled=true ] -->
+<body><script>
+if (window.testRunner) {
+ testRunner.waitUntilDone();
+ testRunner.dumpAsText();
+}
+
+(async () => {
+ const item = new ClipboardItem({
+ "text/plain": new Promise(() => {}),
+ "text/html": Promise.resolve("x"),
+ "text/uri-list": Promise.resolve("http://a/")
+ });
+
+ navigator.clipboard.write([item]).catch(() => {});
+
+ await 0; // Drain microtasks
+
+ navigator.clipboard.write([item]).catch(() => {});
+
+ document.body.innerHTML = '<p>This test passes if WebKit does not hit assertions or crash under ASAN.</p><p>PASS</p>';
+
+ globalThis.testRunner?.notifyDone();
+})();
+</script>
diff --git a/Source/WebCore/Modules/async-clipboard/ClipboardItemBindingsDataSource.cpp b/Source/WebCore/Modules/async-clipboard/ClipboardItemBindingsDataSource.cpp
index 369a9e27a68a..714a57b763ce 100644
--- a/Source/WebCore/Modules/async-clipboard/ClipboardItemBindingsDataSource.cpp
+++ b/Source/WebCore/Modules/async-clipboard/ClipboardItemBindingsDataSource.cpp
@@ -133,10 +133,9 @@ void ClipboardItemBindingsDataSource::getType(const String& type, Ref<DeferredPr
void ClipboardItemBindingsDataSource::clearItemTypeLoaders()
{
- for (auto& itemTypeLoader : m_itemTypeLoaders)
+ auto itemTypeLoaders = std::exchange(m_itemTypeLoaders, { });
+ for (auto& itemTypeLoader : itemTypeLoaders)
itemTypeLoader->invokeCompletionHandler();
-
- m_itemTypeLoaders.clear();
}
void ClipboardItemBindingsDataSource::collectDataForWriting(Clipboard& destination, CompletionHandler<void(std::optional<PasteboardCustomData>)>&& completion)
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to memory corruption
Researchers: dr3dd
Key insight. The trust boundary here is WebContent to GPU process. Because start() re-initialized a source that was already producing data on another thread, a WebContent process could weaponize a flood of StartProducingData messages into a data race. The one-line
if (m_isObservingMedia) return;restores the invariant that setup runs at most once while capturing.
Patch Details
diff --git a/LayoutTests/ipc/usermedia-capture-start-producing-data-race-expected.txt b/LayoutTests/ipc/usermedia-capture-start-producing-data-race-expected.txt
new file mode 100644
index 000000000000..654ddf7f17ef
--- /dev/null
+++ b/LayoutTests/ipc/usermedia-capture-start-producing-data-race-expected.txt
@@ -0,0 +1 @@
+This test passes if it does not crash.
diff --git a/LayoutTests/ipc/usermedia-capture-start-producing-data-race.html b/LayoutTests/ipc/usermedia-capture-start-producing-data-race.html
new file mode 100644
index 000000000000..f681a37dc945
--- /dev/null
+++ b/LayoutTests/ipc/usermedia-capture-start-producing-data-race.html
@@ -0,0 +1,118 @@
+<!DOCTYPE html><!-- webkit-test-runner [ IPCTestingAPIEnabled=true ] -->
+<html>
+<head>
+<script>
+if (window.testRunner) {
+ testRunner.dumpAsText();
+ testRunner.waitUntilDone();
+ testRunner.setUserMediaPermission(true);
+}
+
+function done() {
+ document.body.textContent = "This test passes if it does not crash.";
+ if (window.testRunner)
+ testRunner.notifyDone();
+}
+
+function failTest(error) {
+ document.body.textContent = `Fail: ${error.message}`;
+ if (window.testRunner)
+ testRunner.notifyDone();
+}
+
+async function runTest()
+{
+ if (!window.IPC) {
+ done();
+ return;
+ }
+
+ const { CoreIPC } = await import('./coreipc.js');
+
+ let stream;
+ try {
+ stream = await navigator.mediaDevices.getUserMedia({ audio: true });
+ } catch (error) {
+ throw error;
+ }
+
+ // Hardcoded ID chosen to be much larger than the WebProcess's
+ // RealtimeMediaSourceIdentifier monotonic counter could ever reach in a
+ // single layout-test session, avoiding collision with the id allocated
+ // for the getUserMedia() source above.
+ //
+ // WARNING: RealtimeMediaSourceIdentifier is a process-global monotonic
+ // counter (ObjectIdentifier::generate()) that is never reset across
+ // tests sharing a WebProcess (e.g. mac-wk2). A small hardcoded value
+ // like 100 will eventually collide with the id auto-allocated for the
+ // getUserMedia() source above once enough prior tests have advanced the
+ // counter, tripping ASSERT(!m_proxies.contains(id)) in
+ // UserMediaCaptureManagerProxy::createMediaSourceForCaptureDeviceWithConstraints
+ // and crashing the GPU process on debug bots. Any future IPC test that
+ // hardcodes a RealtimeMediaSourceIdentifier must pick a value well above
+ // any plausible counter (and below the UINT64_MAX hash-traits sentinel).
+ const sourceId = 0xFFFFFFFFn;
+
+ const emptyConstraintMap = {
+ m_width: {}, m_height: {}, m_sampleRate: {}, m_sampleSize: {},
+ m_aspectRatio: {}, m_frameRate: {}, m_volume: {},
+ m_echoCancellation: {}, m_displaySurface: {}, m_logicalSurface: {},
+ m_facingMode: {}, m_deviceId: {}, m_groupId: {}, m_whiteBalanceMode: {},
+ m_zoom: {}, m_torch: {}, m_backgroundBlur: {}, m_powerEfficient: {}
+ };
+
+ let timer;
+ await new Promise((resolve, reject) => {
+ CoreIPC.GPU.UserMediaCaptureManagerProxy.CreateMediaSourceForCaptureDeviceWithConstraints(0, {
+ id: sourceId,
+ device: {
+ persistentId: '239c24b0-2b15-11e3-8224-0800200c9a66',
+ type: 2,
+ label: 'Mock audio device 1',
+ groupId: 'microphonegroup',
+ enabled: true,
+ isDefault: true,
+ isMockDevice: true,
+ isEphemeral: false
+ },
+ hashSalts: { persistentDeviceSalt: 'a', ephemeralDeviceSalt: 'b' },
+ constraints: { mandatoryConstraints: emptyConstraintMap, advancedConstraints: [], isValid: true },
+ shouldUseGPUProcessRemoteFrames: false,
+ pageIdentifier: IPC.pageID
+ }, () => {
+ resolve();
+ });
+
+ timer = setTimeout(() => reject(`Timed out waiting for CreateMediaSourceForCaptureDeviceWithConstraints`), 5000);
+ });
+ clearTimeout(timer);
+
+ CoreIPC.GPU.UserMediaCaptureManagerProxy.StartProducingData(0, { id: sourceId, pageIdentifier: IPC.pageID });
+
+ let count = 0;
+ const interval = setInterval(() => {
+ for (let i = 0; i < 10; i++)
+ CoreIPC.GPU.UserMediaCaptureManagerProxy.StartProducingData(0, { id: sourceId, pageIdentifier: IPC.pageID });
+ if (++count > 20) {
+ clearInterval(interval);
+ stream.getTracks().forEach(t => t.stop());
+
+ // Tear down the GPU-side proxy we created via raw IPC so layout-test
+ // runs that share this WebProcess don't collide on the hardcoded sourceId.
+ CoreIPC.GPU.UserMediaCaptureManagerProxy.RemoveSource(0, { id: sourceId });
+ done();
+ }
+ }, 10);
+}
+
+window.addEventListener('load', async event => {
+ runTest().catch((error) => {
+ failTest(error);
+ });
+});
+
+</script>
+</head>
+<body>
+</body>
+</html>
diff --git a/LayoutTests/platform/glib/TestExpectations b/LayoutTests/platform/glib/TestExpectations
index 11ff43480d47..99c2828898e3 100644
--- a/LayoutTests/platform/glib/TestExpectations
+++ b/LayoutTests/platform/glib/TestExpectations
@@ -3606,6 +3606,7 @@ ipc/pasteboard-write-custom-data.html [ Skip ]
ipc/remotedisplaylistrecorder-drawcontrolpart-slidertrackpart-crash.html [ Skip ]
ipc/media-containment-bypass-create-player.html [ Skip ]
http/tests/ipc/gpu-load-error-empty-user-info-crssh.html [ Skip ]
+ipc/usermedia-capture-start-producing-data-race.html [ Skip ]
# These tests are specific to Cocoa's definition of WebCore::PlatformColorSpace which is different from Skia's.
ipc/decode-feConvolveMatrix-kernelSize-overflow.html [ Skip ]
diff --git a/Source/WebKit/GPUProcess/webrtc/UserMediaCaptureManagerProxy.cpp b/Source/WebKit/GPUProcess/webrtc/UserMediaCaptureManagerProxy.cpp
index 9d2a2f433b78..2c178ee0ba65 100644
--- a/Source/WebKit/GPUProcess/webrtc/UserMediaCaptureManagerProxy.cpp
+++ b/Source/WebKit/GPUProcess/webrtc/UserMediaCaptureManagerProxy.cpp
@@ -140,6 +140,11 @@ class UserMediaCaptureManagerProxySourceProxy final
void start()
{
+ // A compromised WebContent process may send StartProducingData repeatedly. Once we are
+ // observing, prepareAudioDescription() would race the capture thread's audioSamplesAvailable().
+ if (m_isObservingMedia)
+ return;
+
m_shouldReset = true;
m_isStopped = false;
m_source->start();
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may disclose sensitive user information
Researchers: Nan Wang (@eternalsakura13)
Key insight. File-backed blob path validation allowed any path whose parent was the storage directory, so a web process could read other origins’ IndexedDB blob files; restricting access to explicitly allow-listed, result-derived paths restores the boundary.
Patch Details
diff --git a/LayoutTests/ipc/register-file-backed-blob-path-validation-expected.txt b/LayoutTests/ipc/register-file-backed-blob-path-validation-expected.txt
new file mode 100644
index 000000000000..05d932b06d4f
--- /dev/null
+++ b/LayoutTests/ipc/register-file-backed-blob-path-validation-expected.txt
@@ -0,0 +1,2 @@
+Unauthorized file-backed blob registration rejected: PASS
+
diff --git a/LayoutTests/ipc/register-file-backed-blob-path-validation.html b/LayoutTests/ipc/register-file-backed-blob-path-validation.html
new file mode 100644
index 000000000000..1dd09d93dace
--- /dev/null
+++ b/LayoutTests/ipc/register-file-backed-blob-path-validation.html
@@ -0,0 +1,60 @@
+<!DOCTYPE html><!-- webkit-test-runner [ IPCTestingAPIEnabled=true IgnoreInvalidMessageWhenIPCTestingAPIEnabled=true BlobFileAccessEnforcementEnabled=true allowTestOnlyIPC=true ] -->
+<pre id="out"></pre>
+<script>
+if (window.testRunner) {
+ testRunner.dumpAsText();
+ testRunner.waitUntilDone();
+}
+</script>
+<script type="module">
+const out = document.getElementById('out');
+const log = s => { out.textContent += s + '\n'; };
+
+if (!window.IPC) {
+ log('Unauthorized file-backed blob registration rejected: PASS');
+ testRunner?.notifyDone();
+} else {
+
+const { CoreIPC } = await import("./coreipc.js");
+
+function blobSize(u) {
+ return new Promise(res => {
+ CoreIPC.Networking.NetworkConnectionToWebProcess.BlobSize(0, { url: { string: u } },
+ (parsed) => res(parsed && parsed.resultSize !== undefined ? Number(parsed.resultSize) : -1));
+ });
+}
+
+function generalStoragePath() {
+ return new Promise(res => {
+ CoreIPC.Networking.NetworkConnectionToWebProcess.GeneralStoragePathForTesting(0, {},
+ (parsed) => res(parsed && parsed.path ? parsed.path : ''));
+ });
+}
+
+(async () => {
+
+ localStorage.setItem('seed', '1');
+
+ let storagePath = await generalStoragePath();
+ if (!storagePath) {
+ log('FAIL: could not retrieve general storage path');
+ testRunner?.notifyDone();
+ return;
+ }
+
+ let targetPath = storagePath + '/salt';
+ let blobURL = URL.createObjectURL(new Blob([new Uint8Array([0])]));
+ CoreIPC.Networking.NetworkConnectionToWebProcess.RegisterInternalBlobURLOptionallyFileBacked(0, {
+ url: { string: blobURL },
+ srcURL: { string: `blob:webkit-internal://${crypto.randomUUID()}` },
+ fileBackedPath: targetPath,
+ contentType: 'application/octet-stream',
+ });
+ let size = await blobSize(blobURL);
+ log('Unauthorized file-backed blob registration rejected: ' + (size === 1 ? 'PASS' : 'FAIL (BlobSize=' + size + ')'));
+
+ testRunner?.notifyDone();
+})().catch(e => { log('FAIL: ' + e); testRunner?.notifyDone(); });
+
+}
+</script>
diff --git a/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp b/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp
index cf4eec65d80b..6c7c047279cc 100644
--- a/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp
+++ b/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp
@@ -1151,12 +1151,12 @@ void NetworkConnectionToWebProcess::cookieEnabledStateMayHaveChanged()
m_connection->send(Messages::NetworkProcessConnection::UpdateCachedCookiesEnabled(), 0);
}
-bool NetworkConnectionToWebProcess::isFilePathAllowed(NetworkSession& session, String path)
+bool NetworkConnectionToWebProcess::isFilePathAllowed(String path)
{
path = FileSystem::lexicallyNormal(path);
auto parentPath = FileSystem::parentPath(path);
while (parentPath != path) {
- if (m_allowedFilePaths.contains(path) || parentPath == session.storageManager().path() || parentPath == session.storageManager().customIDBStoragePath())
+ if (m_allowedFilePaths.contains(path))
return true;
path = parentPath;
parentPath = FileSystem::parentPath(path);
@@ -1182,7 +1182,7 @@ void NetworkConnectionToWebProcess::registerInternalFileBlobURL(const URL& url,
if (!session)
return;
if (blobFileAccessEnforcementEnabled() && shouldCheckBlobFileAccess())
- MESSAGE_CHECK(isFilePathAllowed(*session, path));
+ MESSAGE_CHECK(isFilePathAllowed(path));
RefPtr sandboxExtension = SandboxExtension::create(WTF::move(extensionHandle));
@@ -1198,7 +1198,7 @@ void NetworkConnectionToWebProcess::registerInternalFileBlobURL(const URL& url,
MESSAGE_CHECK(false);
}
#else
- MESSAGE_CHECK(isFilePathAllowed(*session, replacementPath));
+ MESSAGE_CHECK(isFilePathAllowed(replacementPath));
#endif
}
@@ -1233,7 +1233,7 @@ void NetworkConnectionToWebProcess::registerInternalBlobURLOptionallyFileBacked(
if (!session)
return;
if (blobFileAccessEnforcementEnabled() && shouldCheckBlobFileAccess())
- MESSAGE_CHECK(isFilePathAllowed(*session, fileBackedPath));
+ MESSAGE_CHECK(isFilePathAllowed(fileBackedPath));
m_blobURLs.add({ url, std::nullopt });
session->blobRegistry().registerInternalBlobURLOptionallyFileBacked(url, srcURL, BlobDataFileReferenceWithSandboxExtension::create(fileBackedPath), contentType, { });
@@ -1329,6 +1329,14 @@ void NetworkConnectionToWebProcess::registerBlobPathForTesting(const String& pat
completion();
}
+void NetworkConnectionToWebProcess::generalStoragePathForTesting(CompletionHandler<void(String&&)>&& completion)
+{
+ CheckedPtr session = networkSession();
+ if (!session)
+ return completion({ });
+ completion(String { session->storageManager().path() });
+}
+
void NetworkConnectionToWebProcess::allowAccessToFile(const String& path)
{
m_allowedFilePaths.add(FileSystem::lexicallyNormal(path));
diff --git a/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.h b/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.h
index 827774201378..a9c081afbf41 100644
--- a/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.h
+++ b/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.h
@@ -345,7 +345,8 @@ class NetworkConnectionToWebProcess final
void unregisterBlobURL(const URL&, const std::optional<WebCore::SecurityOriginData>& topOrigin);
void writeBlobsToTemporaryFilesForIndexedDB(const Vector<String>& blobURLs, CompletionHandler<void(Vector<String>&&)>&&);
void registerBlobPathForTesting(const String& path, CompletionHandler<void()>&&);
- bool isFilePathAllowed(NetworkSession&, String path);
+ void generalStoragePathForTesting(CompletionHandler<void(String&&)>&&);
+ bool isFilePathAllowed(String path);
void registerBlobURLHandle(const URL&, const std::optional<WebCore::SecurityOriginData>& topOrigin);
void unregisterBlobURLHandle(const URL&, const std::optional<WebCore::SecurityOriginData>& topOrigin);
diff --git a/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.messages.in b/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.messages.in
index 6310206e0cad..00fe84f0441a 100644
--- a/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.messages.in
+++ b/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.messages.in
@@ -76,6 +76,7 @@ messages -> NetworkConnectionToWebProcess WantsDispatchMessage {
RegisterBlobURLHandle(URL url, std::optional<WebCore::SecurityOriginData> topOrigin);
UnregisterBlobURLHandle(URL url, std::optional<WebCore::SecurityOriginData> topOrigin);
RegisterBlobPathForTesting(String path) -> ();
+ [EnabledBy=AllowTestOnlyIPC] GeneralStoragePathForTesting() -> (String path);
SetCaptureExtraNetworkLoadMetricsEnabled(bool enabled)
diff --git a/Source/WebKit/NetworkProcess/storage/IDBStorageConnectionToClient.cpp b/Source/WebKit/NetworkProcess/storage/IDBStorageConnectionToClient.cpp
index e62fcd676a49..caf7a835ab79 100644
--- a/Source/WebKit/NetworkProcess/storage/IDBStorageConnectionToClient.cpp
+++ b/Source/WebKit/NetworkProcess/storage/IDBStorageConnectionToClient.cpp
@@ -161,14 +161,49 @@ WebIDBResult IDBStorageConnectionToClient::prepareCursorResult(const WebCore::ID
});
}
+static Vector<String> resultBlobFilePaths(const WebCore::IDBResultData& resultData)
+{
+ Vector<String> paths;
+ switch (resultData.type()) {
+ case WebCore::IDBResultType::GetRecordSuccess:
+ case WebCore::IDBResultType::OpenCursorSuccess:
+ case WebCore::IDBResultType::IterateCursorSuccess:
+ paths.appendVector(resultData.getResult().value().blobFilePaths());
+ for (auto& record : resultData.getResult().prefetchedRecords())
+ paths.appendVector(record.value.blobFilePaths());
+ break;
+ case WebCore::IDBResultType::GetAllRecordsSuccess:
+ for (auto& value : resultData.getAllResult().values())
+ paths.appendVector(value.blobFilePaths());
+ break;
+ default:
+ break;
+ }
+ return paths;
+}
+
+template<typename Message>
+void IDBStorageConnectionToClient::sendResultWithBlobFileAccess(WebIDBResult&& result)
+{
+ auto blobFilePaths = resultBlobFilePaths(result.resultData());
+ RefPtr networkStorageManager = m_networkStorageManager.get();
+ if (!networkStorageManager) {
+ IPC::Connection::send(m_connection, Message(WTF::move(result)), 0);
+ return;
+ }
+ networkStorageManager->allowAccessToBlobFilesForProcess(m_identifier, WTF::move(blobFilePaths), [connection = m_connection, result = WTF::move(result)]() mutable {
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: Jonathan Alush-Aben
Key insight. A cross-thread hand-off captured non-thread-safe WTF String/Vector members by reference instead of deep-copying them; even though the caller waits, sharing reference-counted buffers across threads is a use-after-free waiting to happen, and the fix is to pass an isolatedCopy/crossThreadCopy instead.
Patch Details
diff --git a/Source/WebCore/Modules/mediasource/MediaSource.cpp b/Source/WebCore/Modules/mediasource/MediaSource.cpp
index 035b6ad444fb..140425a7964a 100644
--- a/Source/WebCore/Modules/mediasource/MediaSource.cpp
+++ b/Source/WebCore/Modules/mediasource/MediaSource.cpp
@@ -1188,7 +1188,7 @@ bool MediaSource::isTypeSupported(ScriptExecutionContext& context, const String&
}
MediaPlayer::SupportsType supported;
- callOnMainThreadAndWait([&] {
+ callOnMainThreadAndWait([&supported, parameters = crossThreadCopy(WTF::move(parameters))] {
supported = MediaPlayer::supportsType(parameters);
});
diff --git a/Source/WebCore/platform/graphics/MediaPlayer.cpp b/Source/WebCore/platform/graphics/MediaPlayer.cpp
index 352492014a02..21942ae9bd34 100644
--- a/Source/WebCore/platform/graphics/MediaPlayer.cpp
+++ b/Source/WebCore/platform/graphics/MediaPlayer.cpp
@@ -116,6 +116,24 @@ namespace WebCore {
WTF_MAKE_TZONE_ALLOCATED_IMPL(MediaPlayer);
WTF_MAKE_TZONE_ALLOCATED_IMPL(MediaPlayerFactory);
+MediaEngineSupportParameters MediaEngineSupportParameters::isolatedCopy() &&
+{
+ SUPPRESS_UNCOUNTED_ARG return {
+ crossThreadCopy(WTF::move(type)),
+ crossThreadCopy(WTF::move(url)),
+ isMediaSource,
+ isMediaStream,
+ requiresRemotePlayback,
+ supportsLimitedMatroska,
+ crossThreadCopy(WTF::move(contentTypesRequiringHardwareSupport)),
+ crossThreadCopy(WTF::move(allowedMediaContainerTypes)),
+ crossThreadCopy(WTF::move(allowedMediaCodecTypes)),
+ WTF::move(allowedMediaVideoCodecIDs),
+ WTF::move(allowedMediaAudioCodecIDs),
+ WTF::move(allowedMediaCaptionFormatTypes),
+ };
+}
+
// a null player to make MediaPlayer logic simpler
class NullMediaPlayerPrivate final : public MediaPlayerPrivateInterface, public ThreadSafeRefCounted<NullMediaPlayerPrivate, WTF::DestructionThread::Main> {
diff --git a/Source/WebCore/platform/graphics/MediaPlayer.h b/Source/WebCore/platform/graphics/MediaPlayer.h
index 5d8e3b632bdc..a634f3e95955 100644
--- a/Source/WebCore/platform/graphics/MediaPlayer.h
+++ b/Source/WebCore/platform/graphics/MediaPlayer.h
@@ -122,6 +122,8 @@ struct MediaEngineSupportParameters {
std::optional<Vector<FourCC>> allowedMediaVideoCodecIDs;
std::optional<Vector<FourCC>> allowedMediaAudioCodecIDs;
std::optional<Vector<FourCC>> allowedMediaCaptionFormatTypes;
+
+ WARN_UNUSED_RETURN WEBCORE_EXPORT MediaEngineSupportParameters isolatedCopy() &&;
};
struct SeekTarget {
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: A malicious website may exfiltrate data cross-origin
Researchers: Rhyru9, Merrick Hare, Kwak Kiyong, Song Nuri, Khai Tran, John Lussier, Gurpreet Shergill, Drinor Selmanaj (Sentry)
Key insight. WebGPU’s importExternalTexture was a new pixel-ingestion path that never learned the taint rule canvas already enforces. The fix threads the caller’s ScriptExecutionContext (and thus security origin) into the import and rejects tainted video with a SecurityError — closing an SOP hole opened by a newer API surface.
Overview
Impact:
Description: Processing maliciously crafted web content may result in the disclosure of process memory
Researchers: Arni Hardarson, Nathaniel Oh (@calysteon)
Key insight. A single stale offset expression (op.m_checkedOffset - term->inputPosition instead of 0) meant the JIT read the backreference’s character from the wrong input index whenever any term followed the backreference; the character was already correctly addressed by patternIndex, so any additional displacement was pure error.
Patch Details
diff --git a/JSTests/stress/regexp-backreference-unicode-offset.js b/JSTests/stress/regexp-backreference-unicode-offset.js
new file mode 100644
index 000000000000..43d50f0da928
--- /dev/null
+++ b/JSTests/stress/regexp-backreference-unicode-offset.js
@@ -0,0 +1,38 @@
+// Regression test for https://bugs.webkit.org/show_bug.cgi?id=XXXXX
+// Unicode backreference pattern character read offset was wrong when
+// checkedOffset != inputPosition (i.e., when there are terms after the backreference).
+
+function shouldBe(actual, expected, message) {
+ if (actual !== expected)
+ throw new Error("FAIL: " + message + ". Expected: " + JSON.stringify(expected) + ", Actual: " + JSON.stringify(actual));
+}
+
+// Warm up the JIT by running the tests many times.
+for (var i = 0; i < 500; i++) {
+ // Basic case: backreference followed by a literal character.
+ // This triggers checkedOffset != inputPosition for the backreference term.
+ var result = /(.)\1c/u.exec("\u{10000}\u{10000}c");
+ shouldBe(result !== null, true, "/(.)\u005C1c/u should match surrogate pair followed by same pair and 'c'");
+ shouldBe(result[0], "\u{10000}\u{10000}c", "/(.)\u005C1c/u matched string");
+ shouldBe(result[1], "\u{10000}", "/(.)\u005C1c/u capture group");
+
+ // Without trailing term (checkedOffset == inputPosition), this already worked.
+ var result2 = /(.)\1/u.exec("\u{10000}\u{10000}");
+ shouldBe(result2 !== null, true, "/(.)\u005C1/u should match");
+ shouldBe(result2[0], "\u{10000}\u{10000}", "/(.)\u005C1/u matched string");
+
+ // Multiple trailing terms.
+ var result3 = /(.)\1cd/u.exec("\u{10000}\u{10000}cd");
+ shouldBe(result3 !== null, true, "/(.)\u005C1cd/u should match");
+ shouldBe(result3[0], "\u{10000}\u{10000}cd", "/(.)\u005C1cd/u matched string");
+
+ // Case insensitive unicode backreference with trailing term.
+ var result4 = /(.)\1c/iu.exec("aac");
+ shouldBe(result4 !== null, true, "/(.)\u005C1c/iu should match 'aac'");
+ shouldBe(result4[0], "aac", "/(.)\u005C1c/iu matched string");
+
+ // Surrogate pair with case insensitive and trailing term.
+ var result5 = /(.)\1c/iu.exec("\u{10000}\u{10000}c");
+ shouldBe(result5 !== null, true, "/(.)\u005C1c/iu should match surrogate pair");
+ shouldBe(result5[0], "\u{10000}\u{10000}c", "/(.)\u005C1c/iu matched string");
+}
diff --git a/Source/JavaScriptCore/yarr/YarrJIT.cpp b/Source/JavaScriptCore/yarr/YarrJIT.cpp
index 0b50f838ec7d..709d82130f3d 100644
--- a/Source/JavaScriptCore/yarr/YarrJIT.cpp
+++ b/Source/JavaScriptCore/yarr/YarrJIT.cpp
@@ -2011,7 +2011,7 @@ class YarrGenerator final : public YarrJITInfo {
else {
// For reading Unicode characters, use the standard resultReg so we can call the standard tryReadUnicodeChar()
// helper instead of emitting an inlined version.
- readCharacter(op.m_checkedOffset - term->inputPosition, character, patternIndex);
+ readCharacter(0, character, patternIndex);
m_jit.move(character, patternCharacter);
}
#else
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: Юлия Мерцалова
Key insight. WebGL context restoration reinitialized GC-visible context state without holding objectGraphLock(), racing the concurrent GC marker into a use-after-free; taking the lock (and enforcing it via annotations) serializes restore against marking.
Patch Details
diff --git a/LayoutTests/fast/canvas/webgl/context-restore-concurrent-gc-no-crash-expected.txt b/LayoutTests/fast/canvas/webgl/context-restore-concurrent-gc-no-crash-expected.txt
new file mode 100644
index 000000000000..c2541f4f3dd7
--- /dev/null
+++ b/LayoutTests/fast/canvas/webgl/context-restore-concurrent-gc-no-crash-expected.txt
@@ -0,0 +1 @@
+PASS if no crash.
diff --git a/LayoutTests/fast/canvas/webgl/context-restore-concurrent-gc-no-crash.html b/LayoutTests/fast/canvas/webgl/context-restore-concurrent-gc-no-crash.html
new file mode 100644
index 000000000000..a62c55af8994
--- /dev/null
+++ b/LayoutTests/fast/canvas/webgl/context-restore-concurrent-gc-no-crash.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<html>
+<body>
+<!-- Exercises a race between WebGL2 context restoration (which reinitializes
+ bound-object state via initializeNewContext) and concurrent GC marking (which
+ traverses that state via addMembersToOpaqueRoots). Without objectGraphLock()
+ held during restoration, the restore path can free objects the GC marker is
+ still reading, causing a use-after-free. -->
+<script>
+if (window.testRunner) {
+ testRunner.dumpAsText();
+ testRunner.waitUntilDone();
+}
+if (window.internals)
+ internals.settings.setWebGLErrorsToConsoleEnabled(false);
+
+const tick = () => new Promise(r => setTimeout(r, 0));
+
+// Ensure the GC marker doesn't reach the WebGL wrapper until the concurrent phase.
+const heapPaddingSize = 300000;
+const heapPadding = new Array(heapPaddingSize);
+for (let i = 0; i < heapPaddingSize; i++)
+ heapPadding[i] = { a: i, b: { c: i } };
+
+async function loseAndRestoreContext() {
+ const canvas = document.body.appendChild(document.createElement('canvas'));
+ canvas.width = 1;
+ canvas.height = 1;
+ const gl = canvas.getContext('webgl2');
+ const ext = gl.getExtension('WEBGL_lose_context');
+ for (let i = 0; i < heapPaddingSize; i += 4096)
+ heapPadding[i].g = gl;
+
+ const contextLost = new Promise(r => {
+ canvas.addEventListener('webglcontextlost', e => { e.preventDefault(); r(); });
+ });
+ const contextRestored = new Promise(r => {
+ canvas.addEventListener('webglcontextrestored', () => r());
+ });
+
+ ext.loseContext();
+ await contextLost;
+ await tick();
+
+ // Trigger GC so its concurrent marking phase overlaps the restore timer.
+ new WebAssembly.Memory({ initial: 1024 });
+ new WebAssembly.Memory({ initial: 1024 });
+ ext.restoreContext();
+ await tick();
+ await tick();
+ await contextRestored;
+
+ canvas.remove();
+}
+
+async function runTest() {
+ for (let i = 0; i < 50; i++)
+ await loseAndRestoreContext();
+ document.body.textContent = 'PASS if no crash.';
+ if (window.testRunner)
+ testRunner.notifyDone();
+}
+runTest();
+</script>
+</body>
+</html>
diff --git a/Source/WebCore/html/canvas/WebGL2RenderingContext.h b/Source/WebCore/html/canvas/WebGL2RenderingContext.h
index dcf736d39056..b691cd7a4c8b 100644
--- a/Source/WebCore/html/canvas/WebGL2RenderingContext.h
+++ b/Source/WebCore/html/canvas/WebGL2RenderingContext.h
@@ -261,7 +261,7 @@ class WebGL2RenderingContext final : public WebGLRenderingContextBase {
private:
using WebGLRenderingContextBase::WebGLRenderingContextBase;
- void initializeContextState() final;
+ void initializeContextState() WTF_REQUIRES_LOCK(objectGraphLock()) final;
RefPtr<ArrayBufferView> arrayBufferViewSliceFactory(ASCIILiteral functionName, const ArrayBufferView& data, unsigned startByte, unsigned bytelength);
RefPtr<ArrayBufferView> sliceArrayBufferView(ASCIILiteral functionName, const ArrayBufferView& data, GCGLuint srcOffset, GCGLuint length);
@@ -269,7 +269,7 @@ class WebGL2RenderingContext final : public WebGLRenderingContextBase {
long long getInt64Parameter(GCGLenum) final;
Vector<bool> getIndexedBooleanArrayParameter(GCGLenum pname, GCGLuint index);
- void initializeDefaultObjects() final;
+ void initializeDefaultObjects() WTF_REQUIRES_LOCK(objectGraphLock()) final;
void detachAndRemoveAllObjects() WTF_REQUIRES_LOCK(objectGraphLock()) final;
bool validateBufferTarget(ASCIILiteral functionName, GCGLenum target) final;
bool validateBufferTargetCompatibility(ASCIILiteral, GCGLenum, WebGLBuffer*);
diff --git a/Source/WebCore/html/canvas/WebGLRenderingContext.h b/Source/WebCore/html/canvas/WebGLRenderingContext.h
index 7404a87eb9a4..a9b2f1b3e9de 100644
--- a/Source/WebCore/html/canvas/WebGLRenderingContext.h
+++ b/Source/WebCore/html/canvas/WebGLRenderingContext.h
@@ -60,7 +60,7 @@ class WebGLRenderingContext final : public WebGLRenderingContextBase {
private:
using WebGLRenderingContextBase::WebGLRenderingContextBase;
- void initializeDefaultObjects() final;
+ void initializeDefaultObjects() WTF_REQUIRES_LOCK(objectGraphLock()) final;
void detachAndRemoveAllObjects() WTF_REQUIRES_LOCK(objectGraphLock()) final;
};
diff --git a/Source/WebCore/html/canvas/WebGLRenderingContextBase.cpp b/Source/WebCore/html/canvas/WebGLRenderingContextBase.cpp
index f128a2833781..2216b3ef9011 100644
--- a/Source/WebCore/html/canvas/WebGLRenderingContextBase.cpp
+++ b/Source/WebCore/html/canvas/WebGLRenderingContextBase.cpp
@@ -524,8 +524,11 @@ void WebGLRenderingContextBase::initializeNewContext(Ref<GraphicsContextGL> cont
updateActiveOrdinal();
if (!wasActive)
addActiveContext(*this);
- initializeContextState();
- initializeDefaultObjects();
+ {
+ Locker locker { objectGraphLock() };
+ initializeContextState();
+ initializeDefaultObjects();
+ }
// Next calls will receive the context lost callback.
m_context->setClient(this);
}
diff --git a/Source/WebCore/html/canvas/WebGLRenderingContextBase.h b/Source/WebCore/html/canvas/WebGLRenderingContextBase.h
index 57fc93560676..9313e4ff0bca 100644
--- a/Source/WebCore/html/canvas/WebGLRenderingContextBase.h
+++ b/Source/WebCore/html/canvas/WebGLRenderingContextBase.h
@@ -538,8 +538,8 @@ class WebGLRenderingContextBase : public GraphicsContextGL::Client, public GPUBa
friend class ScopedWebGLRestoreTexture;
void initializeNewContext(Ref<GraphicsContextGL>);
- virtual void initializeContextState();
- virtual void initializeDefaultObjects();
+ virtual void initializeContextState() WTF_REQUIRES_LOCK(objectGraphLock());
+ virtual void initializeDefaultObjects() WTF_REQUIRES_LOCK(objectGraphLock());
virtual void detachAndRemoveAllObjects() WTF_REQUIRES_LOCK(objectGraphLock());
// ActiveDOMObject
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: OpenAI Codex Security - Amy Burnett, Khai Tran
Key insight. A validation invariant (reject modules mixing legacy exceptions and try_table) was enforced only on the non-streaming compile path, so streaming compilation accepted modules the engine otherwise forbids — a compile-path parity gap.
Patch Details
diff --git a/JSTests/wasm/stress/streaming-compile-try-table-agreement.js b/JSTests/wasm/stress/streaming-compile-try-table-agreement.js
new file mode 100644
index 000000000000..942083a56858
--- /dev/null
+++ b/JSTests/wasm/stress/streaming-compile-try-table-agreement.js
@@ -0,0 +1,106 @@
+// Streaming and non-streaming WebAssembly compilation should agree on the
+// outcome of a given module: both succeed, or both fail with
+// WebAssembly.CompileError. This test pins that equivalence for a couple of
+// modules that exercise try_table together with other exception handling
+// constructs.
+
+// (module
+// (tag $e)
+// (func (export "run")
+// block $h
+// try_table (catch_all $h)
+// throw $e
+// end
+// end
+// )
+// )
+const tryTableOnly = new Uint8Array([
+ 0x00,0x61,0x73,0x6d, 0x01,0x00,0x00,0x00,
+ 0x01,0x04, 0x01, 0x60,0x00,0x00,
+ 0x03,0x02, 0x01, 0x00,
+ 0x0d,0x03, 0x01, 0x00,0x00,
+ 0x07,0x07, 0x01, 0x03,0x72,0x75,0x6e, 0x00,0x00,
+ 0x0a,0x0f, 0x01, 0x0d,
+ 0x00,
+ 0x02,0x40,
+ 0x1f,0x40, 0x01, 0x02,0x00,
+ 0x08,0x00,
+ 0x0b,
+ 0x0b,
+ 0x0b,
+]);
+
+// (module
+// (tag $e)
+// (func (export "run")
+// try
+// nop
+// catch_all
+// rethrow 0
+// end
+// block $h
+// try_table (catch_all $h)
+// throw $e
+// end
+// end
+// )
+// )
+const mixedEH = new Uint8Array([
+ 0x00,0x61,0x73,0x6d, 0x01,0x00,0x00,0x00,
+ 0x01,0x04, 0x01, 0x60,0x00,0x00,
+ 0x03,0x02, 0x01, 0x00,
+ 0x0d,0x03, 0x01, 0x00,0x00,
+ 0x07,0x07, 0x01, 0x03,0x72,0x75,0x6e, 0x00,0x00,
+ 0x0a,0x16, 0x01, 0x14,
+ 0x00,
+ 0x06,0x40,
+ 0x01,
+ 0x19,
+ 0x09,0x00,
+ 0x0b,
+ 0x02,0x40,
+ 0x1f,0x40, 0x01, 0x02,0x00,
+ 0x08,0x00,
+ 0x0b,
+ 0x0b,
+ 0x0b,
+]);
+
+const cases = [tryTableOnly, mixedEH];
+
+function compileNonStreaming(bytes) {
+ try { new WebAssembly.Module(bytes); return null; }
+ catch (e) { return e; }
+}
+
+async function compileStreaming(bytes) {
+ try {
+ await $vm.createWasmStreamingCompilerForInstantiate(
+ (compiler) => { compiler.addBytes(bytes); });
+ return null;
+ } catch (e) { return e; }
+}
+
+async function main() {
+ for (const bytes of cases) {
+ const ns = compileNonStreaming(bytes);
+ const s = await compileStreaming(bytes);
+
+ const nsFailed = ns !== null;
+ const sFailed = s !== null;
+ if (nsFailed !== sFailed)
+ throw new Error(`compile-path mismatch: non-streaming=${ns}, streaming=${s}`);
+ if (nsFailed) {
+ if (!(ns instanceof WebAssembly.CompileError))
+ throw new Error("non-streaming threw non-CompileError: " + ns);
+ if (!(s instanceof WebAssembly.CompileError))
+ throw new Error("streaming threw non-CompileError: " + s);
+ }
+ }
+}
+
+main().catch(e => {
+ print(String(e));
+ print(String(e.stack));
+ $vm.abort();
+});
diff --git a/Source/JavaScriptCore/wasm/WasmEntryPlan.cpp b/Source/JavaScriptCore/wasm/WasmEntryPlan.cpp
index b6a0ddddbeba..abe34d949a22 100644
--- a/Source/JavaScriptCore/wasm/WasmEntryPlan.cpp
+++ b/Source/JavaScriptCore/wasm/WasmEntryPlan.cpp
@@ -229,10 +229,10 @@ void EntryPlan::compileFunctions()
for (uint32_t index = functionIndex; index < functionIndexEnd; ++index)
compileFunction(FunctionCodeIndex(index));
- if (m_moduleInformation->m_usesModernExceptions.loadRelaxed() && m_moduleInformation->m_usesLegacyExceptions.loadRelaxed()) {
+ {
Locker locker { m_lock };
- fail(makeString("Module uses both legacy exceptions and try_table"_s));
- return;
+ if (failIfMixedExceptionHandlingProposals())
+ return;
}
if (!areWasmToWasmStubsCompiled) {
@@ -267,6 +267,16 @@ void EntryPlan::complete()
}
}
+bool EntryPlan::failIfMixedExceptionHandlingProposals()
+{
+ if (m_moduleInformation->m_usesModernExceptions.loadRelaxed()
+ && m_moduleInformation->m_usesLegacyExceptions.loadRelaxed()) {
+ fail(makeString("Module uses both legacy exceptions and try_table"_s));
+ return true;
+ }
+ return false;
+}
+
bool EntryPlan::completeSyncIfPossible()
{
Locker locker { m_lock };
diff --git a/Source/JavaScriptCore/wasm/WasmEntryPlan.h b/Source/JavaScriptCore/wasm/WasmEntryPlan.h
index d5b458d96fcc..c588ef36cdcb 100644
--- a/Source/JavaScriptCore/wasm/WasmEntryPlan.h
+++ b/Source/JavaScriptCore/wasm/WasmEntryPlan.h
@@ -123,6 +123,8 @@ protected:
bool isComplete() const override { return m_state == State::Completed; }
void complete() WTF_REQUIRES_LOCK(m_lock) override;
+ bool failIfMixedExceptionHandlingProposals() WTF_REQUIRES_LOCK(m_lock);
+
virtual bool prepareImpl() = 0;
virtual void compileFunction(FunctionCodeIndex functionIndex) = 0;
virtual void didCompleteCompilation() WTF_REQUIRES_LOCK(m_lock) = 0;
diff --git a/Source/JavaScriptCore/wasm/WasmIPIntPlan.cpp b/Source/JavaScriptCore/wasm/WasmIPIntPlan.cpp
index a11bfd15c004..b085b37a3d12 100644
--- a/Source/JavaScriptCore/wasm/WasmIPIntPlan.cpp
+++ b/Source/JavaScriptCore/wasm/WasmIPIntPlan.cpp
@@ -166,6 +166,8 @@ void IPIntPlan::didCompleteCompilation()
void IPIntPlan::completeInStreaming()
{
Locker locker { m_lock };
+ if (failIfMixedExceptionHandlingProposals())
+ return;
complete();
}
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: dr3dd
Background
Advisory: Apple Security Advisory
| Attribute | Value |
|---|---|
| CVE | CVE-2026-43746 |
| Bugzilla | 314090 |
| Component | WebKit |
| Bug Class | UAF |
| Severity | medium |
| Advisory | Apple Advisory |
Fix not public. The WebKit fix for this bug is embargoed or not yet disclosed. The bug ID is confirmed from the Apple advisory, but no public commit references it.
Root Cause Analysis
A use-after-free issue was addressed with improved memory management.
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: Idan Masas, DARKNAVY (@DarkNavyOrg), Anonymous working with TrendAI Zero Day Initiative, Daniel Rhea
Key insight. The offset vector was sized from a heuristic (
(subpatterns+1)*2) that no longer matches how the Yarr compiler lays out offsets for duplicate named capture groups; the fix makes the allocation defer to the bytecode’s own recordedm_offsetsSize, the only value guaranteed to match the writer.
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: kwak kiyong / kakaogames
Background
Advisory: Apple Security Advisory
| Attribute | Value |
|---|---|
| CVE | CVE-2026-28883 |
| Bugzilla | 313939 |
| Component | WebKit |
| Bug Class | UAF |
| Severity | medium |
| Advisory | Apple Advisory |
Fix not public. The WebKit fix for this bug is embargoed or not yet disclosed. The bug ID is confirmed from the Apple advisory, but no public commit references it.
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: Aisle offensive security research team (Joshua Rogers, Luigino Camastra, Igor Morgenstern, and Guido Vranken), Maher Azzouzi, Ngan Nguyen of Calif.io
Key insight. The traversal mixed expanded and unexpanded type identities — registering one hash while enqueuing/recursing on the other — so a reachable GC type definition could escape the dependency set and be collected while still live; the fix makes registration and recursion consistently cover both the node and its expansion.
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: Tristan Madani (@TristanInSec) from Talence Security, Nathaniel Oh (@calysteon)
Key insight. Length and backing span must be sampled from a single atomic snapshot when the buffer can grow concurrently; reading
length()beforetypedSpan()created a window a parallel SharedArrayBuffer grow could exploit, and the fix simply derives length from the span taken first.
Patch Details
diff --git a/JSTests/stress/growable-sharedarraybuffer-parallel-grow-during-prototype-methods.js b/JSTests/stress/growable-sharedarraybuffer-parallel-grow-during-prototype-methods.js
index 77e099108dbb..8e172483f870 100644
--- a/JSTests/stress/growable-sharedarraybuffer-parallel-grow-during-prototype-methods.js
+++ b/JSTests/stress/growable-sharedarraybuffer-parallel-grow-during-prototype-methods.js
@@ -28,6 +28,7 @@ for (let round = 0; round < ROUNDS; round++) {
ta.with(0, 0x41414141);
ta.toReversed();
ta.toSorted();
+ ta.sort((a, b) => a - b);
} catch (e) {}
}
diff --git a/Source/JavaScriptCore/runtime/JSGenericTypedArrayViewPrototypeFunctions.h b/Source/JavaScriptCore/runtime/JSGenericTypedArrayViewPrototypeFunctions.h
index 94723d9a2912..d539bfd26201 100644
--- a/Source/JavaScriptCore/runtime/JSGenericTypedArrayViewPrototypeFunctions.h
+++ b/Source/JavaScriptCore/runtime/JSGenericTypedArrayViewPrototypeFunctions.h
@@ -1726,12 +1726,12 @@ static ALWAYS_INLINE EncodedJSValue genericTypedArrayViewProtoFuncSortImpl(VM& v
auto callData = JSC::getCallDataInline(comparatorValue);
- size_t length = thisObject->length();
+ auto originalSpan = thisObject->typedSpan();
+ size_t length = originalSpan.size();
+
if (length < 2)
return JSValue::encode(thisObject);
- auto originalSpan = thisObject->typedSpan();
-
Vector<typename ViewClass::ElementType, 256> vector;
auto totalSize = CheckedSize { length } * 2U;
if (totalSize.hasOverflowed() || !vector.tryGrow(totalSize.value())) [[unlikely]] {
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: Mateusz Krzywicki (iVerify.io)
Key insight. fixObviousSpills clobbered an instruction’s defs uniformly and only after fixing it, ignoring early defs; an operand could be rewritten to a value the instruction’s early def had already overwritten — a JIT aliasing miscompilation. Clobbering early defs before fixing the instruction restores correctness.
Patch Details
diff --git a/JSTests/stress/fixobviousspills-earlydefs.js b/JSTests/stress/fixobviousspills-earlydefs.js
new file mode 100644
index 000000000000..e066e52aca0d
--- /dev/null
+++ b/JSTests/stress/fixobviousspills-earlydefs.js
@@ -0,0 +1,43 @@
+const L = 100;
+const M = 200;
+const S = 0;
+const E = 256;
+const T = (E - S) - 5;
+
+for (let i = 0; i < 2; i++) {
+ let g = (a) => a.size;
+ let d = [];
+
+ for (let c = 0; c < 16; c++) {
+ g(d);
+
+ try {
+ const g = (a) => a.byteLength;
+ const b = new ArrayBuffer(M, { maxByteLength: M });
+ const d = new DataView(b, 0, L);
+
+ for (let c = S; c < E + 11; c++) {
+ g(1);
+
+ if (c != E) {
+ for (let c = S; c < E; c++) {
+ try { if (c == E - T) { g(1); } else { g(d); } } catch(e) { }
+ }
+
+ if (c != E) {
+ for (let c = S; c < E; c++) {
+ try { if (c == E - T) { g(1); } else { g(d); } } catch(e) { }
+ }
+ if (c != E) {
+ for (let c = S; c < E; c++) {
+ try { if (c == E - T) { g(1); } else { g(d); } } catch(e) { }
+ }
+ }
+ }
+ }
+
+ if (c == E) { b.resize(L - 5); } else { g(d); }
+ }
+ } catch (e) { }
+ }
+}
diff --git a/Source/JavaScriptCore/b3/air/AirFixObviousSpills.cpp b/Source/JavaScriptCore/b3/air/AirFixObviousSpills.cpp
index 2fcd2fe1fcec..8e75140f6caf 100644
--- a/Source/JavaScriptCore/b3/air/AirFixObviousSpills.cpp
+++ b/Source/JavaScriptCore/b3/air/AirFixObviousSpills.cpp
@@ -55,9 +55,7 @@ public:
void run()
{
- if (AirFixObviousSpillsInternal::verbose)
- dataLog("Code before fixObviousSpills:\n", m_code);
-
+ dataLogLnIf(AirFixObviousSpillsInternal::verbose, "Code before fixObviousSpills:\n", m_code);
computeAliases();
fixCode();
}
@@ -79,11 +77,13 @@ private:
m_block = block;
m_state = m_atHead[block];
- if (AirFixObviousSpillsInternal::verbose)
- dataLog("Executing block ", *m_block, ": ", m_state, "\n");
-
- for (m_instIndex = 0; m_instIndex < block->size(); ++m_instIndex)
- executeInst();
+ dataLogLnIf(AirFixObviousSpillsInternal::verbose, "Executing block ", *m_block, ": ", m_state);
+ for (m_instIndex = 0; m_instIndex < block->size(); ++m_instIndex) {
+ dataLogLnIf(AirFixObviousSpillsInternal::verbose, " Executing ", m_block->at(m_instIndex), ": ", m_state);
+ clobberEarlyDefs();
+ clobberLateDefs();
+ addInstAliases();
+ }
// Before we call merge we must make sure that the two states are sorted.
m_state.sort();
@@ -116,8 +116,10 @@ private:
RELEASE_ASSERT(m_notBottom.quickGet(block->index()));
for (m_instIndex = 0; m_instIndex < block->size(); ++m_instIndex) {
+ clobberEarlyDefs();
fixInst();
- executeInst();
+ clobberLateDefs();
+ addInstAliases();
}
}
}
@@ -181,27 +183,33 @@ private:
}
}
- void executeInst()
+ void clobberDefs(Inst* prevInst, Inst* nextInst)
{
- Inst& inst = m_block->at(m_instIndex);
-
- if (AirFixObviousSpillsInternal::verbose)
- dataLog(" Executing ", inst, ": ", m_state, "\n");
-
- Inst::forEachDefWithExtraClobberedRegs<Reg>(&inst, &inst,
+ Inst::forEachDefWithExtraClobberedRegs<Reg>(prevInst, nextInst,
[&] (const Reg& reg, Arg::Role, Bank, Width, PreservedWidth) {
- if (AirFixObviousSpillsInternal::verbose)
- dataLog(" Clobbering ", reg, "\n");
+ dataLogLnIf(AirFixObviousSpillsInternal::verbose, " Clobbering ", reg);
m_state.clobber(reg);
});
- Inst::forEachDef<StackSlot*>(&inst, &inst,
+ Inst::forEachDef<StackSlot*>(prevInst, nextInst,
[&] (StackSlot* slot, Arg::Role, Bank, Width) {
- if (AirFixObviousSpillsInternal::verbose)
- dataLog(" Clobbering ", *slot, "\n");
+ dataLogLnIf(AirFixObviousSpillsInternal::verbose, " Clobbering ", *slot);
m_state.clobber(slot);
});
+ }
+
+ void clobberEarlyDefs()
+ {
+ clobberDefs(nullptr, &m_block->at(m_instIndex));
+ }
+ void clobberLateDefs()
+ {
+ clobberDefs(&m_block->at(m_instIndex), nullptr);
+ }
+
+ void addInstAliases()
+ {
forAllAliases(
[&] (const auto& alias) {
m_state.addAlias(alias);
@@ -211,9 +219,7 @@ private:
void fixInst()
{
Inst& inst = m_block->at(m_instIndex);
-
- if (AirFixObviousSpillsInternal::verbose)
- dataLog("Fixing inst ", inst, ": ", m_state, "\n");
+ dataLogLnIf(AirFixObviousSpillsInternal::verbose, "Fixing inst ", inst, ": ", m_state);
// Check if alias analysis says that this is unnecessary.
bool shouldLive = true;
@@ -289,14 +295,12 @@ private:
case Width64:
if (alias->mode != RegSlot::AllBits)
return;
- if (AirFixObviousSpillsInternal::verbose)
- dataLog(" Replacing ", arg, " with ", alias->reg, "\n");
+ dataLogLnIf(AirFixObviousSpillsInternal::verbose, " Replacing ", arg, " with ", alias->reg);
arg = Tmp(alias->reg);
didThings = true;
return;
case Width32:
- if (AirFixObviousSpillsInternal::verbose)
- dataLog(" Replacing ", arg, " with ", alias->reg, " (subwidth case)\n");
+ dataLogLnIf(AirFixObviousSpillsInternal::verbose, " Replacing ", arg, " with ", alias->reg, " (subwidth case)");
arg = Tmp(alias->reg);
didThings = true;
return;
@@ -307,8 +311,7 @@ private:
// Revert to immediate if that didn't work.
if (const SlotConst* alias = m_state.getSlotConst(arg.stackSlot())) {
- if (AirFixObviousSpillsInternal::verbose)
- dataLog(" Replacing ", arg, " with constant ", alias->constant, "\n");
+ dataLogLnIf(AirFixObviousSpillsInternal::verbose, " Replacing ", arg, " with constant ", alias->constant);
if (Arg::isValidImmForm(alias->constant))
arg = Arg::imm(alias->constant);
else
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: Luka Rački
Key insight. YARR accumulated the regexp backtracking call-frame size in a plain unsigned that could overflow, producing an undersized frame the matcher then over-indexed; computing it with CheckedUint32 and rejecting overflow (FrameTooLarge) restores safety.
Patch Details
diff --git a/JSTests/stress/regexp-alternative-heavy.js b/JSTests/stress/regexp-alternative-heavy.js
new file mode 100644
index 000000000000..67fafb77ac15
--- /dev/null
+++ b/JSTests/stress/regexp-alternative-heavy.js
@@ -0,0 +1,41 @@
+//@ runDefault
+
+function tryCompileAndRun(label, buildPattern, testInput) {
+ // print(`\n[*] ${label}`);
+ let pattern;
+ try {
+ pattern = buildPattern();
+ // print(` Pattern length: ${pattern.length} chars`);
+ } catch(e) {
+ print(` BUILD ERROR: ${e.message}`);
+ return;
+ }
+
+ let re;
+ try {
+ re = new RegExp(pattern, 'g');
+ // print(` RegExp constructed OK`);
+ } catch(e) {
+ print(` REGEXP CONSTRUCTION ERROR (likely limit hit): ${e.message}`);
+ return;
+ }
+
+ // Warm the JIT — JSC typically tiered-compiles after ~6 invocations
+ try {
+ for (let i = 0; i < 8; i++) {
+ re.lastIndex = 0;
+ re.exec(testInput);
+ }
+ // print(` exec() completed without crash — check for SP corruption artifacts`);
+ } catch(e) {
+ print(` RUNTIME ERROR: ${e.message}`);
+ }
+}
+
+for (const n of [1000]) {
+ tryCompileAndRun(
+ `Approach C: ${n} alternation groups (a|b)*`,
+ () => '(a|b)*'.repeat(n),
+ 'ab'.repeat(50)
+ );
+}
diff --git a/JSTests/stress/regexp-bol-optimize-out-of-stack.js b/JSTests/stress/regexp-bol-optimize-out-of-stack.js
index 7b99447a6b32..91ee1390ed5b 100644
--- a/JSTests/stress/regexp-bol-optimize-out-of-stack.js
+++ b/JSTests/stress/regexp-bol-optimize-out-of-stack.js
@@ -4,7 +4,7 @@
arrayLength = typeof(arrayLength) === 'undefined' ? 50000 : arrayLength;
-let expectedException = "SyntaxError: Invalid regular expression: regular expression too large";
+let expectedException = "SyntaxError: Invalid regular expression: too many captures";
function test()
{
diff --git a/JSTests/stress/regexp-combined-large.js b/JSTests/stress/regexp-combined-large.js
new file mode 100644
index 000000000000..605736beddc1
--- /dev/null
+++ b/JSTests/stress/regexp-combined-large.js
@@ -0,0 +1,48 @@
+//@ runDefault
+
+function tryCompileAndRun(label, buildPattern, testInput) {
+ // print(`\n[*] ${label}`);
+ let pattern;
+ try {
+ pattern = buildPattern();
+ // print(` Pattern length: ${pattern.length} chars`);
+ } catch(e) {
+ print(` BUILD ERROR: ${e.message}`);
+ return;
+ }
+
+ let re;
+ try {
+ re = new RegExp(pattern, 'g');
+ // print(` RegExp constructed OK`);
+ } catch(e) {
+ print(` REGEXP CONSTRUCTION ERROR (likely limit hit): ${e.message}`);
+ return;
+ }
+
+ // Warm the JIT — JSC typically tiered-compiles after ~6 invocations
+ try {
+ for (let i = 0; i < 8; i++) {
+ re.lastIndex = 0;
+ re.exec(testInput);
+ }
+ // print(` exec() completed without crash — check for SP corruption artifacts`);
+ } catch(e) {
+ print(` RUNTIME ERROR: ${e.message}`);
+ }
+}
+
+const LARGE_N = 12_500_000;
+tryCompileAndRun(
+ `Approach F: ${LARGE_N} flat groups (maximum feasible)`,
+ () => {
+ // Build in chunks to avoid single large string concat
+ let parts = [];
+ const chunk = '(a)*'.repeat(10000); // 40KB per chunk
+ for (let i = 0; i < LARGE_N / 10000; i++) {
+ parts.push(chunk);
+ }
+ return parts.join('');
+ },
+ 'a'
+);
diff --git a/JSTests/stress/regexp-deep-nested.js b/JSTests/stress/regexp-deep-nested.js
new file mode 100644
index 000000000000..d6a80563d9ee
--- /dev/null
+++ b/JSTests/stress/regexp-deep-nested.js
@@ -0,0 +1,41 @@
+//@ runDefault
+
+function tryCompileAndRun(label, buildPattern, testInput) {
+ // print(`\n[*] ${label}`);
+ let pattern;
+ try {
+ pattern = buildPattern();
+ // print(` Pattern length: ${pattern.length} chars`);
+ } catch(e) {
+ print(` BUILD ERROR: ${e.message}`);
+ return;
+ }
+
+ let re;
+ try {
+ re = new RegExp(pattern, 'g');
+ // print(` RegExp constructed OK`);
+ } catch(e) {
+ print(` REGEXP CONSTRUCTION ERROR (likely limit hit): ${e.message}`);
+ return;
+ }
+
+ // Warm the JIT — JSC typically tiered-compiles after ~6 invocations
+ try {
+ for (let i = 0; i < 8; i++) {
+ re.lastIndex = 0;
+ re.exec(testInput);
+ }
+ // print(` exec() completed without crash — check for SP corruption artifacts`);
+ } catch(e) {
+ print(` RUNTIME ERROR: ${e.message}`);
+ }
+}
+
+for (const depth of [100, 500, 1000, 5000, 10000, 50000]) {
+ tryCompileAndRun(
+ `Approach B: nested depth ${depth} — ((((a)*)*...)*)*`,
+ () => '('.repeat(depth) + 'a' + ')*'.repeat(depth),
+ 'a'.repeat(20)
+ );
+}
diff --git a/JSTests/stress/regexp-heavy-mixed.js b/JSTests/stress/regexp-heavy-mixed.js
new file mode 100644
index 000000000000..71bc1776c1f6
--- /dev/null
+++ b/JSTests/stress/regexp-heavy-mixed.js
@@ -0,0 +1,46 @@
+//@ runDefault
+
+function tryCompileAndRun(label, buildPattern, testInput) {
+ // print(`\n[*] ${label}`);
+ let pattern;
+ try {
+ pattern = buildPattern();
+ // print(` Pattern length: ${pattern.length} chars`);
+ } catch(e) {
+ print(` BUILD ERROR: ${e.message}`);
+ return;
+ }
+
+ let re;
+ try {
+ re = new RegExp(pattern, 'g');
+ // print(` RegExp constructed OK`);
+ } catch(e) {
+ print(` REGEXP CONSTRUCTION ERROR (likely limit hit): ${e.message}`);
+ return;
+ }
+
+ // Warm the JIT — JSC typically tiered-compiles after ~6 invocations
+ try {
+ for (let i = 0; i < 8; i++) {
+ re.lastIndex = 0;
+ re.exec(testInput);
+ }
+ // print(` exec() completed without crash — check for SP corruption artifacts`);
+ } catch(e) {
+ print(` RUNTIME ERROR: ${e.message}`);
+ }
+}
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: Yuhao Hu, Yuanming Lai, Chenggang Wu, and Zhe Wang
Key insight. When materializing a sunk PhantomNewArrayWithButterfly on OSR exit, operationPopulateObjectInOSR mishandled elements that were never written, leaving a slot uninitialized instead of the array hole — a garbage JSValue exposed on later access.
Patch Details
diff --git a/JSTests/stress/ftl-osr-exit-phantom-array-unwritten-slot.js b/JSTests/stress/ftl-osr-exit-phantom-array-unwritten-slot.js
new file mode 100644
index 000000000000..a285bbf64df8
--- /dev/null
+++ b/JSTests/stress/ftl-osr-exit-phantom-array-unwritten-slot.js
@@ -0,0 +1,13 @@
+function opt() {
+ const arr = [0,0,0,0,0,0,0];
+ function f() {
+ arr[0];
+ (0)[0];
+ return arr;
+ }
+ const ret = arr.map(f)
+ ret[0] = 1
+}
+for (let i = 0; i < 200; i++) {
+ opt()
+}
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may prevent Content Security Policy from being enforced
Researchers: Cantina
Key insight. CSP path matching must decode per segment (CSP3), not decode the whole path and startsWith; whole-path decoding let %2e%2e/%2f traversal pass the check while the fetch normalized to a blocked path.
Patch Details
diff --git a/Tools/MobileMiniBrowser/MobileMiniBrowserFramework/SceneDelegate.m b/Tools/MobileMiniBrowser/MobileMiniBrowserFramework/SceneDelegate.m
index 0fa324aaec3a..492142235cbf 100644
--- a/Tools/MobileMiniBrowser/MobileMiniBrowserFramework/SceneDelegate.m
+++ b/Tools/MobileMiniBrowser/MobileMiniBrowserFramework/SceneDelegate.m
@@ -42,8 +42,11 @@ - (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session op
UIWindow *window = [[UIWindow alloc] initWithWindowScene:(UIWindowScene *)scene];
self.window = window;
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:[NSBundle bundleForClass:[SceneDelegate class]]];
WebViewController *viewController = (WebViewController *)[storyboard instantiateInitialViewController];
+#pragma clang diagnostic pop
window.rootViewController = viewController;
WKWebsiteDataStore *dataStore = viewController.dataStore;
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: an anonymous researcher
Key insight. ANGLE’s Metal sub-image upload used pixelBytes*width as the row pitch for block-compressed formats, badly under-sizing it and copying out of bounds; computing the pitch with block-aware helpers fixes it.
Patch Details
diff --git a/Source/ThirdParty/ANGLE/src/libANGLE/renderer/metal/TextureMtl.mm b/Source/ThirdParty/ANGLE/src/libANGLE/renderer/metal/TextureMtl.mm
index b0c79d1ab4f6..00858ccfb89a 100644
--- a/Source/ThirdParty/ANGLE/src/libANGLE/renderer/metal/TextureMtl.mm
+++ b/Source/ThirdParty/ANGLE/src/libANGLE/renderer/metal/TextureMtl.mm
@@ -2399,7 +2399,29 @@ angle::Result TextureMtl::convertAndSetPerSliceSubImage(const gl::Context *conte
? imageFormat.textureLoadFunctions(type)
: LoadImageFunctionInfo();
const angle::Format &dstFormat = angle::Format::Get(imageFormat.actualFormatId);
- const size_t dstRowPitch = dstFormat.pixelBytes * mtlArea.size.width;
+ size_t dstRowPitch;
+ size_t dstDepthPitch;
+ if (dstFormat.isBlock)
+ {
+ const gl::InternalFormat &dstFormatInfo =
+ gl::GetSizedInternalFormatInfo(dstFormat.glInternalFormat);
+ GLuint rowPitch;
+ ANGLE_CHECK_GL_MATH(contextMtl,
+ dstFormatInfo.computeCompressedImageRowPitch(
+ static_cast<GLsizei>(mtlArea.size.width), &rowPitch));
+ dstRowPitch = rowPitch;
+
+ GLuint depthPitch;
+ ANGLE_CHECK_GL_MATH(contextMtl, dstFormatInfo.computeCompressedImageDepthPitch(
+ static_cast<GLsizei>(mtlArea.size.height),
+ static_cast<GLuint>(dstRowPitch), &depthPitch));
+ dstDepthPitch = depthPitch;
+ }
+ else
+ {
+ dstRowPitch = dstFormat.pixelBytes * mtlArea.size.width;
+ dstDepthPitch = dstRowPitch * mtlArea.size.height;
+ }
// It is very important to avoid allocating a new buffer for each row during these
// uploads.
@@ -2413,7 +2435,6 @@ angle::Result TextureMtl::convertAndSetPerSliceSubImage(const gl::Context *conte
ASSERT(loadFunctionInfo.loadFunction);
// Need to create a buffer to hold entire decompressed image.
- const size_t dstDepthPitch = dstRowPitch * mtlArea.size.height;
angle::MemoryBuffer decompressBuf;
ANGLE_CHECK_GL_ALLOC(contextMtl,
decompressBuf.resize(dstDepthPitch * mtlArea.size.depth));
@@ -2437,7 +2458,6 @@ angle::Result TextureMtl::convertAndSetPerSliceSubImage(const gl::Context *conte
static_cast<unsigned int>(imageDef.image->sizeAt0().width));
ASSERT(mtlArea.size.height ==
static_cast<unsigned int>(imageDef.image->sizeAt0().height));
- const size_t dstDepthPitch = dstRowPitch * mtlArea.size.height;
ANGLE_TRY(UploadTextureContents(
context, dstFormat, mtlArea, mtl::kZeroNativeMipLevel, slice, pixels,
dstRowPitch, dstDepthPitch, kAvoidStagingBuffers, imageDef.image));
diff --git a/Source/ThirdParty/ANGLE/src/tests/gl_tests/ETCTextureTest.cpp b/Source/ThirdParty/ANGLE/src/tests/gl_tests/ETCTextureTest.cpp
index 319eb1c9ffe7..0d2f732676a4 100644
--- a/Source/ThirdParty/ANGLE/src/tests/gl_tests/ETCTextureTest.cpp
+++ b/Source/ThirdParty/ANGLE/src/tests/gl_tests/ETCTextureTest.cpp
@@ -12,6 +12,7 @@
#endif
#include "test_utils/ANGLETest.h"
+#include "test_utils/gl_raii.h"
#include "media/etc2bc_srgb8_alpha8.inc"
@@ -309,6 +310,45 @@ TEST_P(ETCTextureTest, ETC2SRGB8A1Validation)
}
}
+// Tests that uploading compressed texture from a PBO with a misaligned offset doesn't crash.
+TEST_P(ETCTextureTest, PBOWithMisalignedOffset)
+{
+ // Need ES 3.0 for PBOs.
+ ANGLE_SKIP_TEST_IF(getClientMajorVersion() < 3);
+
+ // Check if ETC2 is supported. Not supported, for example, on ANGLE/OpenGL on macOS.
+ ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_ANGLE_lossy_etc_decode") &&
+ !IsGLExtensionEnabled("GL_OES_compressed_ETC2_RGB8_texture") &&
+ !IsGLExtensionEnabled("GL_ANGLE_compressed_texture_etc"));
+
+ constexpr GLsizei kWidth = 512;
+ constexpr GLsizei kHeight = 512;
+ constexpr GLsizei kBPB = 8; // 8 bytes per block
+ constexpr GLsizei kBW = 4;
+ constexpr GLsizei kBH = 4;
+
+ GLsizei blocksX = kWidth / kBW;
+ GLsizei blocksY = kHeight / kBH;
+ GLsizei compressedSize = blocksX * blocksY * kBPB;
+
+ glBindTexture(GL_TEXTURE_2D, mTexture);
+ // Use GL_COMPRESSED_RGB8_ETC2 which is core in ES 3.0
+ glCompressedTexImage2D(GL_TEXTURE_2D, 0, GL_COMPRESSED_RGB8_ETC2, kWidth, kHeight, 0,
+ compressedSize, nullptr);
+
+ // Misaligned offset to trigger the fallback path in Metal backend
+ constexpr GLsizei kPBOOffset = 1;
+
+ GLBuffer pbo;
+ glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo);
+ glBufferData(GL_PIXEL_UNPACK_BUFFER, kPBOOffset + compressedSize, nullptr, GL_STATIC_DRAW);
+ ASSERT_GL_NO_ERROR();
+
+ glCompressedTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kWidth, kHeight, GL_COMPRESSED_RGB8_ETC2,
+ compressedSize, reinterpret_cast<void *>(kPBOOffset));
+ EXPECT_GL_NO_ERROR();
+}
+
class ETCToBCTextureTest : public ANGLETest<>
{
protected:
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: Vitaly Simonovich
Key insight. Two independent hand-rolled index<->primitive count conversions could disagree or overflow, so the Metal index-rewrite buffer was sized inconsistently with how it was indexed; deriving all counts together in one function removes the mismatch.
Patch Details
diff --git a/Source/ThirdParty/ANGLE/src/libANGLE/renderer/metal/ProvokingVertexHelper.mm b/Source/ThirdParty/ANGLE/src/libANGLE/renderer/metal/ProvokingVertexHelper.mm
index ac00ae604cf9..0faee6c05875 100644
--- a/Source/ThirdParty/ANGLE/src/libANGLE/renderer/metal/ProvokingVertexHelper.mm
+++ b/Source/ThirdParty/ANGLE/src/libANGLE/renderer/metal/ProvokingVertexHelper.mm
@@ -20,84 +20,40 @@ namespace rx
namespace
{
constexpr size_t kInitialIndexBufferSize = 0xFFFF; // Initial 64k pool.
-}
-static inline uint primCountForIndexCount(const uint fixIndexBufferKey, const uint indexCount)
-{
- const uint fixIndexBufferMode =
- (fixIndexBufferKey >> MtlFixIndexBufferKeyModeShift) & MtlFixIndexBufferKeyModeMask;
- switch (fixIndexBufferMode)
- {
- case MtlFixIndexBufferKeyPoints:
- return indexCount;
- case MtlFixIndexBufferKeyLines:
- return indexCount / 2;
- case MtlFixIndexBufferKeyLineStrip:
- return (uint)MAX(0, (int)indexCount - 1);
- case MtlFixIndexBufferKeyLineLoop:
- return (uint)MAX(0, (int)indexCount);
- case MtlFixIndexBufferKeyTriangles:
- return indexCount / 3;
- case MtlFixIndexBufferKeyTriangleStrip:
- return (uint)MAX(0, (int)indexCount - 2);
- case MtlFixIndexBufferKeyTriangleFan:
- return (uint)MAX(0, (int)indexCount - 2);
- default:
- ASSERT(false);
- return 0;
- }
-}
+struct IndexedDrawRewriteInfoMtl {
+ uint32_t primitiveCount;
+ size_t newIndexCount;
+ gl::PrimitiveMode newPrimitiveMode;
+};
-static inline uint indexCountForPrimCount(const uint fixIndexBufferKey, const uint primCount)
+// Returns counts for Metal indexed draw for GL indexed draw parameters.
+inline IndexedDrawRewriteInfoMtl resolveIndexedDrawRewriteInfo(gl::PrimitiveMode mode, GLsizei count)
{
- const uint fixIndexBufferMode =
- (fixIndexBufferKey >> MtlFixIndexBufferKeyModeShift) & MtlFixIndexBufferKeyModeMask;
- switch (fixIndexBufferMode)
+ uint32_t indexCount = static_cast<uint32_t>(count);
+ switch (mode)
{
- case MtlFixIndexBufferKeyPoints:
- return primCount;
- case MtlFixIndexBufferKeyLines:
- return primCount * 2;
- case MtlFixIndexBufferKeyLineStrip:
- return primCount * 2;
- case MtlFixIndexBufferKeyLineLoop:
- return primCount * 2;
- case MtlFixIndexBufferKeyTriangles:
- return primCount * 3;
- case MtlFixIndexBufferKeyTriangleStrip:
- return primCount * 3;
- case MtlFixIndexBufferKeyTriangleFan:
- return primCount * 3;
+ case gl::PrimitiveMode::Points:
+ return {indexCount, indexCount, gl::PrimitiveMode::Points};
+ case gl::PrimitiveMode::Lines:
+ return {indexCount / 2, indexCount - (indexCount % 2), gl::PrimitiveMode::Lines};
+ case gl::PrimitiveMode::LineLoop:
+ return {indexCount, static_cast<size_t>(indexCount) * 2, gl::PrimitiveMode::Lines};
+ case gl::PrimitiveMode::LineStrip:
+ indexCount = indexCount < 1 ? 0 : indexCount - 1;
+ return {indexCount, static_cast<size_t>(indexCount) * 2, gl::PrimitiveMode::Lines};
+ case gl::PrimitiveMode::Triangles:
+ return {indexCount / 3, indexCount - (indexCount % 3), gl::PrimitiveMode::Triangles};
+ case gl::PrimitiveMode::TriangleStrip:
+ case gl::PrimitiveMode::TriangleFan:
+ indexCount = indexCount < 2 ? 0 : indexCount - 2;
+ return {indexCount, static_cast<size_t>(indexCount) * 3, gl::PrimitiveMode::Triangles};
default:
- ASSERT(false);
- return 0;
+ UNREACHABLE();
+ return {0, 0, gl::PrimitiveMode::InvalidEnum};
}
}
-static inline gl::PrimitiveMode getNewPrimitiveMode(const uint fixIndexBufferKey)
-{
- const uint fixIndexBufferMode =
- (fixIndexBufferKey >> MtlFixIndexBufferKeyModeShift) & MtlFixIndexBufferKeyModeMask;
- switch (fixIndexBufferMode)
- {
- case MtlFixIndexBufferKeyPoints:
- return gl::PrimitiveMode::Points;
- case MtlFixIndexBufferKeyLines:
- return gl::PrimitiveMode::Lines;
- case MtlFixIndexBufferKeyLineStrip:
- return gl::PrimitiveMode::Lines;
- case MtlFixIndexBufferKeyLineLoop:
- return gl::PrimitiveMode::Lines;
- case MtlFixIndexBufferKeyTriangles:
- return gl::PrimitiveMode::Triangles;
- case MtlFixIndexBufferKeyTriangleStrip:
- return gl::PrimitiveMode::Triangles;
- case MtlFixIndexBufferKeyTriangleFan:
- return gl::PrimitiveMode::Triangles;
- default:
- ASSERT(false);
- return gl::PrimitiveMode::InvalidEnum;
- }
}
ProvokingVertexHelper::ProvokingVertexHelper(ContextMtl *context) : mIndexBuffers(false)
@@ -191,14 +147,13 @@ angle::Result ProvokingVertexHelper::preconditionIndexBuffer(ContextMtl *context
// Upload index buffer
// dispatch per-primitive?
uint indexBufferKey = buildIndexBufferKey(elementsType, primitiveRestartEnabled, primitiveMode);
- uint primCount = primCountForIndexCount(indexBufferKey, (uint32_t)count);
- uint newIndexCount = indexCountForPrimCount(indexBufferKey, primCount);
+ auto [primCount, newIndexCount, newPrimitiveMode] = resolveIndexedDrawRewriteInfo(primitiveMode, count);
// We do not support large buffers at the moment.
ANGLE_CHECK_GL_MATH(context, newIndexCount <= std::numeric_limits<uint32_t>::max());
- size_t indexSize = gl::GetDrawElementsTypeSize(elementsType);
- size_t newOffset = 0;
+ size_t newIndexBufferSize = newIndexCount << gl::GetDrawElementsTypeShift(elementsType);
+ size_t newOffset = 0;
mtl::BufferRef newBuffer;
- ANGLE_TRY(mIndexBuffers.allocate(context, newIndexCount * indexSize + indexOffset, nullptr,
+ ANGLE_TRY(mIndexBuffers.allocate(context, newIndexBufferSize + indexOffset, nullptr,
&newBuffer, &newOffset));
auto threadsPerThreadgroup = MTLSizeMake(MIN(primCount, 64u), 1, 1);
@@ -217,7 +172,7 @@ angle::Result ProvokingVertexHelper::preconditionIndexBuffer(ContextMtl *context
threadsPerThreadgroup);
outIndexCount = static_cast<uint32_t>(newIndexCount);
outIndexOffset = newOffset;
- outPrimitiveMode = getNewPrimitiveMode(indexBufferKey);
+ outPrimitiveMode = newPrimitiveMode;
outNewBuffer = newBuffer;
return angle::Result::Continue;
}
@@ -237,14 +192,13 @@ angle::Result ProvokingVertexHelper::generateIndexBuffer(ContextMtl *context,
// dispatch per-primitive?
const bool primitiveRestartEnabled = false;
uint indexBufferKey = buildIndexBufferKey(elementsType, primitiveRestartEnabled, primitiveMode);
- uint primCount = primCountForIndexCount(indexBufferKey, (uint32_t)count);
- uint newIndexCount = indexCountForPrimCount(indexBufferKey, primCount);
+ auto [primCount, newIndexCount, newPrimitiveMode] = resolveIndexedDrawRewriteInfo(primitiveMode, count);
// We do not support large buffers at the moment.
ANGLE_CHECK_GL_MATH(context, newIndexCount <= std::numeric_limits<uint32_t>::max());
- size_t indexSize = gl::GetDrawElementsTypeSize(elementsType);
+ size_t newIndexBufferSize = newIndexCount << gl::GetDrawElementsTypeShift(elementsType);
size_t newIndexOffset = 0;
mtl::BufferRef newBuffer;
- ANGLE_TRY(mIndexBuffers.allocate(context, newIndexCount * indexSize, nullptr, &newBuffer,
+ ANGLE_TRY(mIndexBuffers.allocate(context, newIndexBufferSize, nullptr, &newBuffer,
&newIndexOffset));
auto threadsPerThreadgroup = MTLSizeMake(MIN(primCount, 64u), 1, 1);
@@ -263,7 +217,7 @@ angle::Result ProvokingVertexHelper::generateIndexBuffer(ContextMtl *context,
threadsPerThreadgroup);
outIndexCount = static_cast<uint32_t>(newIndexCount);
outIndexOffset = newIndexOffset;
- outPrimitiveMode = getNewPrimitiveMode(indexBufferKey);
+ outPrimitiveMode = newPrimitiveMode;
outNewBuffer = newBuffer;
return angle::Result::Continue;
}
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: Milad Nasr and Nicholas Carlini with Claude, Anthropic
Key insight. The dialog close command used a value string tied to the invoker button’s attribute while dispatching author-observable events that can mutate that attribute; snapshotting the value into a local before the events fire removes the dangling use.
Patch Details
diff --git a/LayoutTests/fast/html/dialog-close-from-button-crash-expected.txt b/LayoutTests/fast/html/dialog-close-from-button-crash-expected.txt
new file mode 100644
index 000000000000..d6d0936dfab3
--- /dev/null
+++ b/LayoutTests/fast/html/dialog-close-from-button-crash-expected.txt
@@ -0,0 +1,4 @@
+This tests removing the value attribute of a button as it tries to close the dialog element.
+WebKit should not crash or hit any assertions under ASAN.
+
+PASS
diff --git a/LayoutTests/fast/html/dialog-close-from-button-crash.html b/LayoutTests/fast/html/dialog-close-from-button-crash.html
new file mode 100644
index 000000000000..ac956c9a8823
--- /dev/null
+++ b/LayoutTests/fast/html/dialog-close-from-button-crash.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+<body>
+<p>This tests removing the value attribute of a button as it tries to close the dialog element.<br>
+WebKit should not crash or hit any assertions under ASAN.</p>
+<dialog id=dialog open>
+ <button command=close commandfor=dialog>Close</button>
+</dialog>
+<script>
+window?.testRunner?.dumpAsText();
+window?.GCController?.collect();
+const button = document.querySelector('button');
+button.setAttribute('value', 'PA' + 'SS');
+document.querySelector('dialog').addEventListener('beforetoggle', (event) => {
+ button.removeAttribute('value');
+});
+button.click();
+document.write(dialog.returnValue);
+</script>
+</body>
+</html>
diff --git a/Source/WebCore/html/HTMLDialogElement.cpp b/Source/WebCore/html/HTMLDialogElement.cpp
index 0b39c2a2f885..e0ef22618ef9 100644
--- a/Source/WebCore/html/HTMLDialogElement.cpp
+++ b/Source/WebCore/html/HTMLDialogElement.cpp
@@ -305,11 +305,13 @@ bool HTMLDialogElement::handleCommandInternal(HTMLButtonElement& invoker, const
if (isOpen()) {
if (command == CommandType::Close) {
- close(invoker.value().string(), &invoker);
+ String value = invoker.value().string();
+ close(value, &invoker);
return true;
}
if (command == CommandType::RequestClose) {
- requestClose(invoker.value().string(), &invoker);
+ String value = invoker.value().string();
+ requestClose(value, &invoker);
return true;
}
} else {
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: Kenneth Hsu of Palo Alto Networks, Jérôme DJOUDER, dr3dd
Key insight. The write-back path of an encoded-media transform trusted script to return only well-typed frames belonging to that transformer; the fix enforces that trust boundary by tagging each frame with its originating transformer and rejecting any written frame whose media type or origin does not match.
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: Gia Bui (@yabeow) from Calif.io, dr3dd, w0wbox
Key insight. The date-field shadow element called into its input-type owner through a non-owning weak/raw reference, so changing the input’s type mid-event freed the owner while it was still being used; making the owner ref-counted and upgrading to a RefPtr across each call closes the UAF.
Patch Details
diff --git a/LayoutTests/fast/forms/date/date-editable-components/date-editable-components-change-type-on-input-event-expected.txt b/LayoutTests/fast/forms/date/date-editable-components/date-editable-components-change-type-on-input-event-expected.txt
new file mode 100644
index 000000000000..d71615565c02
--- /dev/null
+++ b/LayoutTests/fast/forms/date/date-editable-components/date-editable-components-change-type-on-input-event-expected.txt
@@ -0,0 +1,11 @@
+Test that changing the input type during an input event does not crash.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+PASS input.type is "text"
+PASS Did not crash.
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/fast/forms/date/date-editable-components/date-editable-components-change-type-on-input-event.html b/LayoutTests/fast/forms/date/date-editable-components/date-editable-components-change-type-on-input-event.html
new file mode 100644
index 000000000000..79d7c2bae618
--- /dev/null
+++ b/LayoutTests/fast/forms/date/date-editable-components/date-editable-components-change-type-on-input-event.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src="../../../../resources/js-test.js"></script>
+<script src="../../../../resources/ui-helper.js"></script>
+</head>
+<body>
+
+<input id="input" type="date">
+
+<script>
+
+jsTestIsAsync = true;
+
+description("Test that changing the input type during an input event does not crash.");
+
+input.addEventListener("input", () => {
+ input.type = "text";
+});
+
+addEventListener("load", async () => {
+ input.focus(); // [mm]/dd/yyyy
+ UIHelper.keyDown("9"); // -> [09]/dd/yyyy
+ UIHelper.keyDown("rightArrow"); // -> 09/[dd]/yyyy
+ UIHelper.keyDown("2"); // -> 09/[02]/yyyy
+ UIHelper.keyDown("rightArrow"); // -> 09/02/[yyyy]
+ UIHelper.keyDown("2"); // -> 09/02/[0002]
+ UIHelper.keyDown("0"); // -> 09/02/[0020]
+ UIHelper.keyDown("1"); // -> 09/02/[0201]
+ UIHelper.keyDown("2"); // -> 09/02/[2012] -> input event fires -> type changes to text
+
+ shouldBeEqualToString("input.type", "text");
+
+ testPassed("Did not crash.");
+ finishJSTest();
+});
+
+</script>
+</body>
+</html>
diff --git a/Source/WebCore/html/BaseDateAndTimeInputType.cpp b/Source/WebCore/html/BaseDateAndTimeInputType.cpp
index dac2e40936bb..a1c91bb15fd1 100644
--- a/Source/WebCore/html/BaseDateAndTimeInputType.cpp
+++ b/Source/WebCore/html/BaseDateAndTimeInputType.cpp
@@ -650,9 +650,10 @@ void BaseDateAndTimeInputType::didEndChooser()
bool BaseDateAndTimeInputType::setupDateTimeChooserParameters(DateTimeChooserParameters& parameters)
{
- ASSERT(element());
+ RefPtr element = this->element();
+ if (!element)
+ return false;
- Ref element = *this->element();
Ref document = element->document();
if (!document->view())
diff --git a/Source/WebCore/html/BaseDateAndTimeInputType.h b/Source/WebCore/html/BaseDateAndTimeInputType.h
index 1320307f2af9..3cffb02f4731 100644
--- a/Source/WebCore/html/BaseDateAndTimeInputType.h
+++ b/Source/WebCore/html/BaseDateAndTimeInputType.h
@@ -55,6 +55,9 @@ public:
bool typeMismatch() const final;
bool hasBadInput() const final;
+ void ref() const final { InputType::ref(); }
+ void deref() const final { InputType::deref(); }
+
protected:
enum class DateTimeFormatValidationResults : uint8_t {
HasYear = 1 << 0,
diff --git a/Source/WebCore/html/shadow/DateTimeEditElement.cpp b/Source/WebCore/html/shadow/DateTimeEditElement.cpp
index 9a0788a123c9..3f1bea1dc35d 100644
--- a/Source/WebCore/html/shadow/DateTimeEditElement.cpp
+++ b/Source/WebCore/html/shadow/DateTimeEditElement.cpp
@@ -236,9 +236,10 @@ size_t DateTimeEditElement::fieldIndexOf(const DateTimeFieldElement& fieldToFind
void DateTimeEditElement::defaultEventHandler(Event& event)
{
if (RefPtr keyboardEvent = dynamicDowncast<KeyboardEvent>(event)) {
- if (keyboardEvent->keyIdentifier() == "U+0020"_s && m_editControlOwner) {
+ RefPtr editControlOwner = m_editControlOwner;
+ if (editControlOwner && keyboardEvent->keyIdentifier() == "U+0020"_s) {
// Forward space keypresses to the owner to activate the date picker.
- m_editControlOwner->didReceiveSpaceKeyFromControl();
+ editControlOwner->didReceiveSpaceKeyFromControl();
// We want to mark the event as handled to avoid scrolling the page.
event.setDefaultHandled();
return;
@@ -314,7 +315,8 @@ void DateTimeEditElement::layout(const LayoutParameters& layoutParameters)
void DateTimeEditElement::didBlurFromField(Event& event)
{
- if (!m_editControlOwner)
+ RefPtr editControlOwner = m_editControlOwner;
+ if (!editControlOwner)
return;
if (RefPtr newFocusedElement = event.relatedTarget()) {
@@ -326,13 +328,13 @@ void DateTimeEditElement::didBlurFromField(Event& event)
return;
}
- m_editControlOwner->didBlurFromControl();
+ editControlOwner->didBlurFromControl();
}
void DateTimeEditElement::fieldValueChanged()
{
- if (m_editControlOwner)
- m_editControlOwner->didChangeValueFromControl();
+ if (RefPtr editControlOwner = m_editControlOwner)
+ editControlOwner->didChangeValueFromControl();
}
bool DateTimeEditElement::focusOnNextFocusableField(size_t startIndex)
@@ -382,12 +384,16 @@ bool DateTimeEditElement::focusOnPreviousField(const DateTimeFieldElement& field
bool DateTimeEditElement::isFieldOwnerDisabled() const
{
- return m_editControlOwner && m_editControlOwner->isEditControlOwnerDisabled();
+ if (RefPtr editControlOwner = m_editControlOwner)
+ return editControlOwner->isEditControlOwnerDisabled();
+ return false;
}
bool DateTimeEditElement::isFieldOwnerReadOnly() const
{
- return m_editControlOwner && m_editControlOwner->isEditControlOwnerReadOnly();
+ if (RefPtr editControlOwner = m_editControlOwner)
+ return editControlOwner->isEditControlOwnerReadOnly();
+ return false;
}
bool DateTimeEditElement::isFieldOwnerHorizontal() const
@@ -399,18 +405,22 @@ bool DateTimeEditElement::isFieldOwnerHorizontal() const
bool DateTimeEditElement::didFieldOwnerTransferFocusToPicker()
{
- return m_editControlOwner && m_editControlOwner->didEditControlOwnerTransferFocusToPicker();
+ if (RefPtr editControlOwner = m_editControlOwner)
+ return editControlOwner->didEditControlOwnerTransferFocusToPicker();
+ return false;
}
void DateTimeEditElement::didSuppressBlurDueToPickerFocusTransfer()
{
- if (m_editControlOwner)
- m_editControlOwner->didSuppressBlurDueToPickerFocusTransfer();
+ if (RefPtr editControlOwner = m_editControlOwner)
+ editControlOwner->didSuppressBlurDueToPickerFocusTransfer();
}
AtomString DateTimeEditElement::localeIdentifier() const
{
- return m_editControlOwner ? m_editControlOwner->localeIdentifier() : nullAtom();
+ if (RefPtr editControlOwner = m_editControlOwner)
+ return editControlOwner->localeIdentifier();
+ return nullAtom();
}
const GregorianDateTime& DateTimeEditElement::placeholderDate() const
@@ -439,12 +449,16 @@ void DateTimeEditElement::setEmptyValue(const LayoutParameters& layoutParameters
String DateTimeEditElement::value() const
{
- return m_editControlOwner ? m_editControlOwner->formatDateTimeFieldsState(valueAsDateTimeFieldsState()) : emptyString();
+ if (RefPtr editControlOwner = m_editControlOwner)
+ return editControlOwner->formatDateTimeFieldsState(valueAsDateTimeFieldsState());
+ return emptyString();
}
String DateTimeEditElement::placeholderValue() const
{
- return m_editControlOwner ? m_editControlOwner->formatDateTimeFieldsState(valueAsDateTimeFieldsState(DateTimePlaceholderIfNoValue::Yes)) : emptyString();
+ if (RefPtr editControlOwner = m_editControlOwner)
+ return editControlOwner->formatDateTimeFieldsState(valueAsDateTimeFieldsState(DateTimePlaceholderIfNoValue::Yes));
+ return emptyString();
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: dr3dd
Key insight. The anchor must be released before the structures that depend on it; tearing it down last left it usable after related state began being freed.
Patch Details
diff --git a/JSTests/wasm/stress/instance-anchor.js b/JSTests/wasm/stress/instance-anchor.js
new file mode 100644
index 000000000000..b2b59cb78edd
--- /dev/null
+++ b/JSTests/wasm/stress/instance-anchor.js
@@ -0,0 +1,42 @@
+//@ runDefault("--jitPolicyScale=0.1")
+/*
+(module
+ (func (export "foo") (result i32)
+ i32.const 42
+ )
+)
+*/
+
+const WASM_CODE = new Uint8Array([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x05, 0x01, 0x60, 0x00, 0x01, 0x7f, 0x03, 0x02, 0x01, 0x00, 0x07, 0x07, 0x01, 0x03, 0x66, 0x6f, 0x6f, 0x00, 0x00, 0x0a, 0x06, 0x01, 0x04, 0x00, 0x41, 0x2a, 0x0b]);
+
+function bury(f, n) {
+ if (n === 0) {
+ return f();
+ }
+
+ return bury(f, n - 1);
+}
+
+function main() {
+ const mod = new WebAssembly.Module(WASM_CODE);
+
+ function warmUpInstanceB() {
+ const instanceB = new WebAssembly.Instance(mod);
+
+ instanceB.exports.foo();
+ }
+
+ bury(warmUpInstanceB, 500);
+
+ const instanceA = new WebAssembly.Instance(mod);
+
+ for (let i = 0; i < 500; i++)
+ instanceA.exports.foo();
+
+ gc();
+
+ print("done (should have crashed above)");
+
+}
+
+main();
diff --git a/Source/JavaScriptCore/wasm/js/JSWebAssemblyInstance.cpp b/Source/JavaScriptCore/wasm/js/JSWebAssemblyInstance.cpp
index 230024a44607..88208bd7b14a 100644
--- a/Source/JavaScriptCore/wasm/js/JSWebAssemblyInstance.cpp
+++ b/Source/JavaScriptCore/wasm/js/JSWebAssemblyInstance.cpp
@@ -163,6 +163,11 @@ void JSWebAssemblyInstance::finishCreation(VM& vm)
JSWebAssemblyInstance::~JSWebAssemblyInstance()
{
+ if (m_anchor) {
+ m_anchor->tearDown();
+ m_anchor = nullptr;
+ }
+
m_vm->traps().unregisterMirror(m_stackMirror);
clearJSCallICs(*m_vm);
@@ -174,11 +179,6 @@ JSWebAssemblyInstance::~JSWebAssemblyInstance()
for (auto& slot : baselineDatas())
std::destroy_at(&slot);
-
- if (m_anchor) {
- m_anchor->tearDown();
- m_anchor = nullptr;
- }
}
void JSWebAssemblyInstance::destroy(JSCell* cell)
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: Maher Azzouzi
Key insight. This is a stack/frame-size boundary check failure in WebAssembly function entry: a function declaring an enormous number of locals can produce a frame that overruns a smaller worker-thread stack when the stack-limit check is missing or mis-computed, turning a should-be-catchable resource error into a process crash (mechanism inferred; only the regression test is in this commit).
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: wac and Kookhwan Lee working with TrendAI Zero Day Initiative
Key insight. The inspector CSS agent aliased element->styleResolver() (and ancestors) by bare reference across style-resolving calls that can free it, so a style recalc mid-walk left a dangling resolver; retaining them with Ref<> fixes the lifetime.
Patch Details
diff --git a/Source/WebCore/inspector/agents/InspectorCSSAgent.cpp b/Source/WebCore/inspector/agents/InspectorCSSAgent.cpp
index 7447712ac885..d0ca0727b86d 100644
--- a/Source/WebCore/inspector/agents/InspectorCSSAgent.cpp
+++ b/Source/WebCore/inspector/agents/InspectorCSSAgent.cpp
@@ -494,8 +494,8 @@ Inspector::Protocol::ErrorStringOr<std::tuple<RefPtr<JSON::ArrayOf<Inspector::Pr
}
// Matched rules.
- auto& styleResolver = element->styleResolver();
- auto matchedRules = styleResolver.pseudoStyleRulesForElement(element, elementPseudoId, Style::Resolver::AllCSSRules);
+ Ref styleResolver = element->styleResolver();
+ auto matchedRules = styleResolver->pseudoStyleRulesForElement(element, elementPseudoId, Style::Resolver::AllCSSRules);
auto matchedCSSRules = buildArrayForMatchedRuleList(matchedRules, styleResolver, *element, elementPseudoId);
RefPtr<JSON::ArrayOf<Inspector::Protocol::CSS::PseudoIdMatches>> pseudoElements;
RefPtr<JSON::ArrayOf<Inspector::Protocol::CSS::InheritedStyleEntry>> inherited;
@@ -521,7 +521,8 @@ Inspector::Protocol::ErrorStringOr<std::tuple<RefPtr<JSON::ArrayOf<Inspector::Pr
continue;
if (auto protocolPseudoId = protocolValueForPseudoElementType(pseudoElementType)) {
- auto matchedRules = styleResolver.pseudoStyleRulesForElement(element, pseudoElementType, Style::Resolver::AllCSSRules);
+ Ref styleResolver = element->styleResolver();
+ auto matchedRules = styleResolver->pseudoStyleRulesForElement(element, pseudoElementType, Style::Resolver::AllCSSRules);
if (!matchedRules.isEmpty()) {
auto matches = Inspector::Protocol::CSS::PseudoIdMatches::create()
.setPseudoId(protocolPseudoId.value())
@@ -535,9 +536,10 @@ Inspector::Protocol::ErrorStringOr<std::tuple<RefPtr<JSON::ArrayOf<Inspector::Pr
if (!includeInherited || *includeInherited) {
inherited = JSON::ArrayOf<Inspector::Protocol::CSS::InheritedStyleEntry>::create();
- for (auto& ancestor : ancestorsOfType<Element>(*element)) {
- auto& parentStyleResolver = ancestor.styleResolver();
- auto parentMatchedRules = parentStyleResolver.styleRulesForElement(&ancestor, Style::Resolver::AllCSSRules);
+ for (Ref ancestor : ancestorsOfType<Element>(*element)) {
+ Ref parentStyleResolver = ancestor->styleResolver();
+ auto parentMatchedRules = parentStyleResolver->styleRulesForElement(ancestor.ptr(), Style::Resolver::AllCSSRules);
+ Ref styleResolver = element->styleResolver();
auto entry = Inspector::Protocol::CSS::InheritedStyleEntry::create()
.setMatchedCSSRules(buildArrayForMatchedRuleList(parentMatchedRules, styleResolver, ancestor, { }))
.release();
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: An app may be able to access sensitive user data
Researchers: Cantina
Key insight. SameSite classification must be computed against the request’s real initiator; omitting the initiator collapses to a classification that can treat cross-site loads as same-site and leak SameSite=Strict cookies.
Patch Details
diff --git a/Source/WebCore/loader/MixedContentChecker.cpp b/Source/WebCore/loader/MixedContentChecker.cpp
index bd42f3d7c2bd..f7c90310966e 100644
--- a/Source/WebCore/loader/MixedContentChecker.cpp
+++ b/Source/WebCore/loader/MixedContentChecker.cpp
@@ -131,10 +131,6 @@ bool MixedContentChecker::canModifyRequest(const URL& url, FetchOptions::Destina
bool MixedContentChecker::shouldBlockRequest(Frame& frame, const URL& url, IsUpgradable isUpgradable)
{
- RefPtr<Document> document;
- if (auto* localFrame = dynamicDowncast<LocalFrame>(frame))
- document = localFrame->document();
-
#if ENABLE(CONTENT_FILTERING) && HAVE(WEBCONTENTRESTRICTIONS)
if (url == ContentFilter::blockedPageURL())
return false;
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may disclose sensitive user information
Researchers: Luke Francis, Vaagn Vardanian, kwak kiyong / kakaogames, Vitaly Simonovich, Adel Bouachraoui, greenbynox
Key insight. File paths that cross from WebContent to the UI process for attachments must be validated against what the UI process actually vended; trusting web-process-supplied paths permits traversal to arbitrary local files.
Patch Details
diff --git a/Source/WebCore/platform/audio/ios/MediaDeviceRouteController.mm b/Source/WebCore/platform/audio/ios/MediaDeviceRouteController.mm
index 77ece021b66d..bafd2772a91a 100644
--- a/Source/WebCore/platform/audio/ios/MediaDeviceRouteController.mm
+++ b/Source/WebCore/platform/audio/ios/MediaDeviceRouteController.mm
@@ -31,10 +31,12 @@
#if ENABLE(WIRELESS_PLAYBACK_MEDIA_PLAYER)
+#import "Logging.h"
#import "MediaDeviceRoute.h"
#import "MediaSessionHelperIOS.h"
#import "MediaStrategy.h"
#import "PlatformStrategies.h"
+#import <wtf/darwin/DispatchExtras.h>
#import <WebKitAdditions/MediaDeviceRouteControllerAdditions.mm>
#import <pal/ios/AVRoutingSoftLink.h>
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: A malicious iframe may use another website’s download settings
Researchers: Khiem Tran
Key insight. Download decisions were not gated on the completion of the navigation’s own Safe Browsing check, so a malicious iframe could slip a download past the warning; deferring the download until the per-navigation check completes and honoring a match closes it.
Patch Details
diff --git a/Source/WebKit/UIProcess/API/APINavigation.cpp b/Source/WebKit/UIProcess/API/APINavigation.cpp
index a65c4a21dfe6..28f2960c47ab 100644
--- a/Source/WebKit/UIProcess/API/APINavigation.cpp
+++ b/Source/WebKit/UIProcess/API/APINavigation.cpp
@@ -187,6 +187,21 @@ void Navigation::setSafeBrowsingWarning(RefPtr<WebKit::BrowsingWarning>&& safeBr
m_safeBrowsingWarning = WTF::move(safeBrowsingWarning);
}
+void Navigation::whenSafeBrowsingCheckCompletes(Function<void()>&& callback)
+{
+ if (!safeBrowsingCheckOngoing()) {
+ callback();
+ return;
+ }
+ m_safeBrowsingCheckCompletionCallbacks.append(WTF::move(callback));
+}
+
+void Navigation::fireSafeBrowsingCheckCompletionCallbacks()
+{
+ for (auto& callback : std::exchange(m_safeBrowsingCheckCompletionCallbacks, { }))
+ callback();
+}
+
size_t Navigation::redirectChainIndex(const WTF::URL& url)
{
size_t index = m_redirectChain.find(url);
diff --git a/Source/WebKit/UIProcess/API/APINavigation.h b/Source/WebKit/UIProcess/API/APINavigation.h
index 6ae3d0fa8e37..96ec09585e65 100644
--- a/Source/WebKit/UIProcess/API/APINavigation.h
+++ b/Source/WebKit/UIProcess/API/APINavigation.h
@@ -188,6 +188,8 @@ public:
bool NODELETE safeBrowsingCheckOngoing();
void setSafeBrowsingWarning(RefPtr<WebKit::BrowsingWarning>&&);
RefPtr<WebKit::BrowsingWarning> NODELETE safeBrowsingWarning();
+ void whenSafeBrowsingCheckCompletes(Function<void()>&&);
+ void fireSafeBrowsingCheckCompletionCallbacks();
void setSafeBrowsingCheckTimedOut() { m_safeBrowsingCheckTimedOut = true; }
bool safeBrowsingCheckTimedOut() { return m_safeBrowsingCheckTimedOut; }
bool hadSafeBrowsingWarning() const { return m_hadSafeBrowsingWarning; }
@@ -250,6 +252,7 @@ private:
MonotonicTime m_requestStart { MonotonicTime::now() };
RefPtr<WebKit::BrowsingWarning> m_safeBrowsingWarning;
ListHashSet<size_t> m_ongoingSafeBrowsingChecks;
+ Vector<Function<void()>> m_safeBrowsingCheckCompletionCallbacks;
RefPtr<WebKit::FrameProcess> m_pendingSharedProcess;
RefPtr<WebKit::FrameState> m_backForwardFrameState;
};
diff --git a/Source/WebKit/UIProcess/Cocoa/WebPageProxyCocoa.mm b/Source/WebKit/UIProcess/Cocoa/WebPageProxyCocoa.mm
index e0ce751cde8e..d20e7d987e3e 100644
--- a/Source/WebKit/UIProcess/Cocoa/WebPageProxyCocoa.mm
+++ b/Source/WebKit/UIProcess/Cocoa/WebPageProxyCocoa.mm
@@ -326,6 +326,9 @@ void WebPageProxy::beginSafeBrowsingCheck(const URL& url, API::Navigation& navig
}
}
+ if (!navigation->safeBrowsingCheckOngoing())
+ navigation->fireSafeBrowsingCheckCompletionCallbacks();
+
if (!navigation->safeBrowsingCheckOngoing() && navigation->safeBrowsingWarning() && navigation->safeBrowsingCheckTimedOut()) {
protectedThis->setHasShownSafeBrowsingWarningAfterLastLoadCommit();
protectedThis->showBrowsingWarning(navigation->safeBrowsingWarning());
diff --git a/Source/WebKit/UIProcess/WebPageProxy.cpp b/Source/WebKit/UIProcess/WebPageProxy.cpp
index 5270ec0eabe5..30de1e0e1e55 100644
--- a/Source/WebKit/UIProcess/WebPageProxy.cpp
+++ b/Source/WebKit/UIProcess/WebPageProxy.cpp
@@ -9405,6 +9405,54 @@ void WebPageProxy::decidePolicyForNavigationAction(Ref<WebProcessProxy>&& proces
protectedPageClient->clearBrowsingWarning();
+ if (policyAction == PolicyAction::Download && navigation->safeBrowsingCheckOngoing()) {
+ navigation->whenSafeBrowsingCheckCompletes([
+ this, protectedThis = WTF::move(protectedThis), navigation, completionHandlerWrapper = WTF::move(completionHandlerWrapper),
+ frame, frameInfo = WTF::move(frameInfo), protectedPageClient = WTF::move(protectedPageClient)
+ ] mutable {
+ if (RefPtr safeBrowsingWarning = navigation->safeBrowsingWarning()) {
+ navigation->setSafeBrowsingWarning(nullptr);
+ if (!frame->isMainFrame()) {
+ auto error = interruptedForPolicyChangeError(navigation->currentRequest());
+ m_navigationClient->didFailProvisionalNavigationWithError(*this, FrameInfoData { frameInfo }, navigation.get(), navigation->currentRequest().url(), error, nullptr);
+ WEBPAGEPROXY_RELEASE_LOG(Loading, "decidePolicyForNavigationAction: Ignoring download because Safe Browsing found a match.");
+ completionHandlerWrapper(PolicyAction::Ignore);
+ return;
+ }
+
+ Ref protectedPageLoadState = pageLoadState();
+ auto transaction = protectedPageLoadState->transaction();
+ protectedPageLoadState->setTitleFromBrowsingWarning(transaction, safeBrowsingWarning->title());
+
+ protectedPageClient->showBrowsingWarning(*safeBrowsingWarning, [protectedThis = WTF::move(protectedThis), completionHandlerWrapper = WTF::move(completionHandlerWrapper), protectedPageClient](auto&& result) mutable {
+ Ref protectedPageLoadState = protectedThis->pageLoadState();
+ auto transaction = protectedPageLoadState->transaction();
+ protectedPageLoadState->setTitleFromBrowsingWarning(transaction, { });
+
+ switchOn(result, [&](const URL& url) {
+ completionHandlerWrapper(PolicyAction::Ignore);
+ protectedThis->loadRequest({ URL { url } });
+ }, [&protectedThis, &completionHandlerWrapper](ContinueUnsafeLoad continueUnsafeLoad) {
+ switch (continueUnsafeLoad) {
+ case ContinueUnsafeLoad::No:
+ if (!protectedThis->hasCommittedAnyProvisionalLoads())
+ protectedThis->m_uiClient->close(protectedThis.ptr());
+ completionHandlerWrapper(PolicyAction::Ignore);
+ break;
+ case ContinueUnsafeLoad::Yes:
+ completionHandlerWrapper(PolicyAction::Download);
+ break;
+ }
+ });
+ });
+ m_uiClient->didShowSafeBrowsingWarning();
+ return;
+ }
+ completionHandlerWrapper(PolicyAction::Download);
+ });
+ return;
+ }
+
if (RefPtr safeBrowsingWarning = navigation->safeBrowsingWarning()) {
navigation->setSafeBrowsingWarning(nullptr);
if (frame->isMainFrame() && safeBrowsingWarning->url().isValid()) {
@@ -9727,9 +9775,43 @@ void WebPageProxy::decidePolicyForResponseShared(Ref<WebProcessProxy>&& process,
#if USE(QUICK_LOOK) && ENABLE(QUICKLOOK_SANDBOX_RESTRICTIONS)
bool supportsMIMEType = PreviewConverter::supportsMIMEType(navigationResponse->response().mimeType());
#endif
- auto completionHandlerWrapper = [navigation, protectedThis, request, navigationResponse = WTF::move(navigationResponse), frameInfo = WTF::move(frameInfo), completionHandler = WTF::move(completionHandler)] (PolicyAction policyAction) mutable {
+ auto completionHandlerWrapper = [navigation, protectedThis, request, navigationResponse = WTF::move(navigationResponse), completionHandler = WTF::move(completionHandler)](PolicyAction policyAction) mutable {
protectedThis->receivedNavigationResponsePolicyDecision(policyAction, navigation.get(), request, WTF::move(navigationResponse), WTF::move(completionHandler));
};
+ if (policyAction == PolicyAction::Download && navigation && navigation->safeBrowsingCheckOngoing()) {
+ navigation->whenSafeBrowsingCheckCompletes([
+ this, protectedThis = WTF::move(protectedThis), navigation, completionHandlerWrapper = WTF::move(completionHandlerWrapper),
+ frame, frameInfo = WTF::move(frameInfo), request
+ ] mutable {
+ if (RefPtr safeBrowsingWarning = navigation->safeBrowsingWarning()) {
+ if (!frame->isMainFrame()) {
+ auto error = interruptedForPolicyChangeError(navigation->currentRequest());
+ m_navigationClient->didFailProvisionalNavigationWithError(*this, FrameInfoData { frameInfo }, navigation.get(), request.url(), error, nullptr);
+ completionHandlerWrapper(PolicyAction::Ignore);
+ return;
+ }
+ Ref protectedPageLoadState = pageLoadState();
+ auto transaction = protectedPageLoadState->transaction();
+ protectedPageLoadState->setTitleFromBrowsingWarning(transaction, safeBrowsingWarning->title());
+ navigation->setSafeBrowsingWarning(nullptr);
+ protect(protectedThis->pageClient())->showBrowsingWarning(*safeBrowsingWarning, [protectedThis = WTF::move(protectedThis), completionHandlerWrapper = WTF::move(completionHandlerWrapper)](auto&& result) mutable {
+ switchOn(result, [&](const URL& url) {
+ completionHandlerWrapper(PolicyAction::Ignore);
+ protectedThis->loadRequest(URL { url });
+ }, [&](ContinueUnsafeLoad continueUnsafeLoad) {
+ if (continueUnsafeLoad == ContinueUnsafeLoad::No)
+ completionHandlerWrapper(PolicyAction::Ignore);
+ else
+ completionHandlerWrapper(PolicyAction::Download);
+ });
+ });
+ m_uiClient->didShowSafeBrowsingWarning();
+ return;
+ }
+ completionHandlerWrapper(PolicyAction::Download);
+ });
+ return;
+ }
if (navigation && navigation->safeBrowsingWarning()) {
RefPtr safeBrowsingWarning = navigation->safeBrowsingWarning();
if (frame->isMainFrame() && safeBrowsingWarning->url().isValid()) {
diff --git a/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/SafeBrowsing.mm b/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/SafeBrowsing.mm
index 4b417cd08322..c4894846a476 100644
--- a/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/SafeBrowsing.mm
+++ b/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/SafeBrowsing.mm
@@ -1194,4 +1194,184 @@ TEST(SafeBrowsing, SetTimeoutNavigationFromWarningPage)
EXPECT_FALSE(didCloseCalled);
}
+TEST(SafeBrowsing, DownloadDeferredAndBlockedBySafeBrowsing)
+{
+ DelayedLookupContext.delayDuration = 50_ms;
+ TestWebKitAPI::HTTPServer server({
+ { "/malicious"_s, { "download-content"_s } },
+ }, TestWebKitAPI::HTTPServer::Protocol::HttpsProxy);
+ RetainPtr configuration = server.httpsProxyConfiguration();
+
+ ClassMethodSwizzler swizzler(getSSBLookupContextClassSingleton(), @selector(sharedLookupContext), [DelayedLookupContext methodForSelector:@selector(sharedLookupContext)]);
+
+ RetainPtr webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]);
+ [webView configuration].preferences.fraudulentWebsiteWarningEnabled = YES;
+ RetainPtr delegate = adoptNS([TestNavigationDelegate new]);
+ [delegate allowAnyTLSCertificate];
+
+ __block bool downloadStarted = false;
+ delegate.get().decidePolicyForNavigationResponse = ^(WKNavigationResponse *, void (^decisionHandler)(WKNavigationResponsePolicy)) {
+ decisionHandler(WKNavigationResponsePolicyDownload);
+ };
+ delegate.get().navigationResponseDidBecomeDownload = ^(WKNavigationResponse *, WKDownload *) {
+ downloadStarted = true;
+ };
+
+ [webView setNavigationDelegate:delegate.get()];
+ [webView evaluateJavaScript:@"window.location = 'https://example2.com/malicious'" completionHandler:nil];
+
+ while (![webView _safeBrowsingWarning])
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: Artem Dinaburg of Trail of Bits via Anthropic CVD
Key insight. OSR-exit array materialization assumed the compile-time indexing type still held, but a ‘having a bad time’ transition racing the exit switched arrays to SlowPutArrayStorage, creating a Structure/butterfly layout mismatch; materialization must pick the layout consistently and convert if bad-time is active.
Patch Details
diff --git a/JSTests/stress/ftl-osr-exit-phantom-new-array-with-butterfly-having-a-bad-time.js b/JSTests/stress/ftl-osr-exit-phantom-new-array-with-butterfly-having-a-bad-time.js
new file mode 100644
index 000000000000..2611454cf304
--- /dev/null
+++ b/JSTests/stress/ftl-osr-exit-phantom-new-array-with-butterfly-having-a-bad-time.js
@@ -0,0 +1,35 @@
+//@ runDefault("--jitPolicyScale=0.1")
+
+let trigger = false;
+
+function cb() {
+ if (trigger) {
+ Object.defineProperty(Array.prototype, 0, {
+ get() { return 42; }, configurable: true
+ });
+ }
+}
+noInline(cb);
+
+function collect() { gc(); }
+noInline(collect);
+
+function opt() {
+ let a = new Array(5);
+ a[0] = 1.1;
+ a[1] = 2.2;
+ a[2] = 3.3;
+ a[3] = 4.4;
+ a[4] = 5.5;
+ cb();
+ collect();
+ return a[0] + a[1] + a[2] + a[3] + a[4];
+}
+noInline(opt);
+
+for (let i = 0; i < 1000; i++)
+ opt();
+
+trigger = true;
+opt();
+gc();
diff --git a/Source/JavaScriptCore/ftl/FTLOperations.cpp b/Source/JavaScriptCore/ftl/FTLOperations.cpp
index d02c76c8578e..4b4d59d41117 100644
--- a/Source/JavaScriptCore/ftl/FTLOperations.cpp
+++ b/Source/JavaScriptCore/ftl/FTLOperations.cpp
@@ -112,10 +112,17 @@ JSC_DEFINE_NOEXCEPT_JIT_OPERATION(operationPopulateObjectInOSR, void, (JSGlobalO
// empty JSValue is not an Int32. Contiguous is also handled here for the debug ASSERT
// in putDirectIndex that null-derefs on the empty JSValue. Write directly into the
// butterfly to preserve the indexing type.
+ //
+ // If the VM had a bad time between FTL compilation and this OSR exit, the Array was
+ // switched to SlowPutArrayStorage in operationMaterializeObjectInOSR. A hole (empty
+ // JSValue) must then be cleared in the ArrayStorage vector rather than the contiguous
+ // butterfly to match the rematerialized layout.
if (hasDouble(array->indexingType()) && value.isNumber() && std::isnan(value.asNumber())) [[unlikely]]
array->butterfly()->contiguousDouble().atUnsafe(index) = PNaN;
else if ((hasInt32(array->indexingType()) || hasContiguous(array->indexingType())) && !value) [[unlikely]]
array->butterfly()->contiguous().atUnsafe(index).setStartingValue(JSValue());
+ else if (hasAnyArrayStorage(array->indexingType()) && !value) [[unlikely]]
+ array->butterfly()->arrayStorage()->m_vector[index].clear();
else
array->putDirectIndex(globalObject, index, value);
@@ -308,7 +315,13 @@ JSC_DEFINE_NOEXCEPT_JIT_OPERATION(operationMaterializeObjectInOSR, HeapCell*, (J
}
case PhantomNewArrayWithButterfly: {
- Structure* structure = globalObject->arrayStructureForIndexingTypeDuringAllocation(materialization->indexingType());
+ // Rematerialized butterflies are always non-ArrayStorage. However, isHavingABadTime could
+ // have become true between the FTL compilation and the rematerialization, which would have
+ // switched arrayStructureForIndexingTypeDuringAllocation to SlowPutArrayStorage for all
+ // indexing types. To avoid a layout mismatch, the original Array structure is used to
+ // rematerialize the Array initially. If we're having a bad time, the layout is switched to
+ // SlowPutArrayStorage below.
+ Structure* structure = globalObject->originalArrayStructureForIndexingType(materialization->indexingType());
Butterfly* butterfly = nullptr;
for (unsigned i = 0; i < materialization->properties().size(); ++i) {
@@ -349,6 +362,14 @@ JSC_DEFINE_NOEXCEPT_JIT_OPERATION(operationMaterializeObjectInOSR, HeapCell*, (J
butterfly->contiguous().atUnsafe(index).setStartingValue(jsNumber(sentinel));
}
+ if (globalObject->isHavingABadTime()) [[unlikely]] {
+#if ASSERT_ENABLED
+ Structure* originalStructure = globalObject->arrayStructureForIndexingTypeDuringAllocation(materialization->indexingType());
+ ASSERT(!originalStructure || hasSlowPutArrayStorage(originalStructure->indexingType()));
+#endif
+ result->switchToSlowPutArrayStorage(vm);
+ }
+
return result;
}
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: Do Young Park
Key insight. When repatching a callee’s callsites, keep the owning object (BBQCallee) alive for the whole operation; pinning only the weakly-held OMGOSREntryCallee missed the case where the owner is freed.
Patch Details
diff --git a/Source/JavaScriptCore/wasm/WasmCalleeGroup.cpp b/Source/JavaScriptCore/wasm/WasmCalleeGroup.cpp
index 3b271fc5daf3..4cf13d7638e8 100644
--- a/Source/JavaScriptCore/wasm/WasmCalleeGroup.cpp
+++ b/Source/JavaScriptCore/wasm/WasmCalleeGroup.cpp
@@ -345,9 +345,18 @@ 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 the OMGOSREntryCallee could die between the time we collect the callsites and when
- // we actually repatch its callsites.
+ // 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.
+
// FIXME: These inline capacities were picked semi-randomly. We should figure out if there's a better number.
+ Vector<Ref<BBQCallee>, 4> keepAliveBBQCallees;
Vector<Ref<OMGOSREntryCallee>, 4> keepAliveOSREntryCallees;
Vector<Callsite, 16> callsites;
@@ -373,6 +382,8 @@ 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.
@@ -384,6 +395,8 @@ void CalleeGroup::updateCallsitesToCallUs(const AbstractLocker& locker, CodeLoca
if (bbqCallee) {
collectCallsites(bbqCallee.get());
ASSERT(!bbqCallee->osrEntryCallee() || m_osrEntryCallees.find(callerIndex) != m_osrEntryCallees.end());
+ keepAliveBBQCallees.append(bbqCallee.releaseNonNull());
+ bbqCalleeKeptAlive = true;
}
#endif
#if ENABLE(WEBASSEMBLY_OMGJIT)
@@ -391,7 +404,17 @@ void CalleeGroup::updateCallsitesToCallUs(const AbstractLocker& locker, CodeLoca
if (auto iter = m_osrEntryCallees.find(callerIndex); iter != m_osrEntryCallees.end()) {
if (RefPtr callee = iter->value.get()) {
collectCallsites(callee.get());
- keepAliveOSREntryCallees.append(callee.releaseNonNull());
+ // 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());
} else
m_osrEntryCallees.remove(iter);
}
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may prevent Content Security Policy from being enforced
Researchers: Cantina
Key insight. Propagating CSP with setHTTPHeaderField overwrote earlier policies, so a document with multiple CSP headers passed only the last one to inheriting blob:/local-scheme frames; appending with addHTTPHeaderField preserves all of them.
Patch Details
diff --git a/LayoutTests/http/tests/security/contentSecurityPolicy/blob-url-inherits-multiple-csp-policies-expected.txt b/LayoutTests/http/tests/security/contentSecurityPolicy/blob-url-inherits-multiple-csp-policies-expected.txt
new file mode 100644
index 000000000000..228b9da58211
--- /dev/null
+++ b/LayoutTests/http/tests/security/contentSecurityPolicy/blob-url-inherits-multiple-csp-policies-expected.txt
@@ -0,0 +1,14 @@
+CONSOLE MESSAGE: Refused to execute a script because its hash, its nonce, or 'unsafe-inline' does not appear in the script-src directive of the Content Security Policy.
+Tests that a blob: URL iframe inherits ALL CSP policies from its creator document, not just the last one. When the creator has two enforced CSP headers where one blocks inline scripts, inline scripts inside the blob document should be blocked.
+
+
+
+--------
+Frame: '<!--frame1-->'
+--------
+
+
+--------
+Frame: '<!--frame2-->'
+--------
+PASS: Inline script was blocked by CSP.
diff --git a/LayoutTests/http/tests/security/contentSecurityPolicy/blob-url-inherits-multiple-csp-policies.html b/LayoutTests/http/tests/security/contentSecurityPolicy/blob-url-inherits-multiple-csp-policies.html
new file mode 100644
index 000000000000..3b21b13e864c
--- /dev/null
+++ b/LayoutTests/http/tests/security/contentSecurityPolicy/blob-url-inherits-multiple-csp-policies.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+if (window.testRunner) {
+ testRunner.dumpAsText();
+ testRunner.dumpChildFramesAsText();
+ testRunner.waitUntilDone();
+}
+setTimeout(function() {
+ if (window.testRunner)
+ testRunner.notifyDone();
+}, 1000);
+</script>
+</head>
+<body>
+<p>Tests that a blob: URL iframe inherits ALL CSP policies from its creator document,
+not just the last one. When the creator has two enforced CSP headers where one blocks
+inline scripts, inline scripts inside the blob document should be blocked.</p>
+<iframe src="http://127.0.0.1:8000/security/contentSecurityPolicy/resources/echo-multiple-csp-blob-iframe.py"></iframe>
+</body>
+</html>
diff --git a/LayoutTests/http/tests/security/contentSecurityPolicy/resources/create-blob-iframe.js b/LayoutTests/http/tests/security/contentSecurityPolicy/resources/create-blob-iframe.js
new file mode 100644
index 000000000000..3d28c62bbbdf
--- /dev/null
+++ b/LayoutTests/http/tests/security/contentSecurityPolicy/resources/create-blob-iframe.js
@@ -0,0 +1,12 @@
+var iframe = document.getElementById("blob-frame");
+var html = [
+ "<!DOCTYPE html>",
+ "<html><body>",
+ "<p id='result'>PASS: Inline script was blocked by CSP.</p>",
+ "<script>",
+ "document.getElementById('result').textContent = 'FAIL: Inline script executed (CSP policy was dropped).';",
+ "</" + "script>",
+ "</body></html>"
+].join("\n");
+var blob = new Blob([html], { type: "text/html" });
+iframe.src = URL.createObjectURL(blob);
diff --git a/LayoutTests/http/tests/security/contentSecurityPolicy/resources/echo-multiple-csp-blob-iframe.py b/LayoutTests/http/tests/security/contentSecurityPolicy/resources/echo-multiple-csp-blob-iframe.py
new file mode 100755
index 000000000000..923aa9170663
--- /dev/null
+++ b/LayoutTests/http/tests/security/contentSecurityPolicy/resources/echo-multiple-csp-blob-iframe.py
@@ -0,0 +1,17 @@
+#!/usr/bin/env python3
+
+import sys
+
+sys.stdout.write(
+ 'Content-Type: text/html; charset=UTF-8\r\n'
+ "Content-Security-Policy: script-src 'self'; frame-src blob:; default-src 'self'\r\n"
+ "Content-Security-Policy: script-src 'self' 'unsafe-inline'; frame-src blob:; default-src 'self'\r\n"
+ '\r\n'
+ '<!DOCTYPE html>\n'
+ '<html>\n'
+ '<body>\n'
+ '<iframe id="blob-frame"></iframe>\n'
+ '<script src="/security/contentSecurityPolicy/resources/create-blob-iframe.js"></script>\n'
+ '</body>\n'
+ '</html>\n'
+)
diff --git a/LayoutTests/imported/w3c/web-platform-tests/trusted-types/inheriting-csp-for-local-schemes-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/trusted-types/inheriting-csp-for-local-schemes-expected.txt
index 47396d14dd48..1a48f1b35af2 100644
--- a/LayoutTests/imported/w3c/web-platform-tests/trusted-types/inheriting-csp-for-local-schemes-expected.txt
+++ b/LayoutTests/imported/w3c/web-platform-tests/trusted-types/inheriting-csp-for-local-schemes-expected.txt
@@ -4,7 +4,7 @@ PASS trusted-types directive should be inherited in local srcdoc frames
PASS require-trusted-types-for directive should be inherited in local data frames
PASS trusted-types directive should be inherited in local data frames
PASS require-trusted-types-for directive should be inherited in local blob frames
-FAIL trusted-types directive should be inherited in local blob frames assert_not_equals: got disallowed value null
+PASS trusted-types directive should be inherited in local blob frames
PASS require-trusted-types-for directive should be inherited in local about:blank frames
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may bypass Content Security Policy
Researchers: lebr0nli of National Yang Ming Chiao Tung University, Security and Systems Lab
Background
Advisory: Apple Security Advisory
| Attribute | Value |
|---|---|
| CVE | CVE-2026-43670 |
| Bugzilla | 309004 |
| Component | WebKit |
| Bug Class | CSP Bypass |
| Severity | high |
| Advisory | Apple Advisory |
Fix not public. The WebKit fix for this bug is embargoed or not yet disclosed. The bug ID is confirmed from the Apple advisory, but no public commit references it.
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: Yeonghyeon Choi, Daniel Rhea, Söhnke Benedikt Fischedick (Tripton), Emrovsky & Switch3301, Yevhen Pervushyn
Key insight. Growing a WebAssembly.Memory exposed as a resizable ArrayBuffer moved the backing store but did not refresh the data pointers cached in its typed-array views, leaving them dangling; refreshing every view’s m_vector to the new base after a grow fixes it.
Overview
Impact:
Description: Processing maliciously crafted web content may prevent Content Security Policy from being enforced
Researchers: webb
Key insight. A security-context inheritance path was gated only on the existence of a cached HistoryItem policy container, not on the navigation actually being a back/forward traversal, so ordinary (non-history) loads such as dynamically-set srcdoc iframes silently adopted a stale/permissive policy container instead of the parent’s CSP.
Patch Details
diff --git a/LayoutTests/http/tests/security/contentSecurityPolicy/iframe-srcdoc-import-bypass-expected.txt b/LayoutTests/http/tests/security/contentSecurityPolicy/iframe-srcdoc-import-bypass-expected.txt
new file mode 100644
index 000000000000..fdd0c9b0110f
--- /dev/null
+++ b/LayoutTests/http/tests/security/contentSecurityPolicy/iframe-srcdoc-import-bypass-expected.txt
@@ -0,0 +1,14 @@
+CONSOLE MESSAGE: Refused to apply a stylesheet because its hash, its nonce, or 'unsafe-inline' appears in neither the style-src directive nor the default-src directive of the Content Security Policy.
+CONSOLE MESSAGE: Refused to load https://localhost:8443/security/contentSecurityPolicy/resources/module-pass.py because it does not appear in the script-src directive of the Content Security Policy.
+CONSOLE MESSAGE: Refused to load https://localhost:8443/security/contentSecurityPolicy/resources/module-pass.py because it does not appear in the script-src directive of the Content Security Policy.
+This test checks if iframes without an initial srcdoc attribute incorrectly bypass CSP when srcdoc is set later.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+PASS iframe1Loaded is false
+PASS iframe2Loaded is false
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/http/tests/security/contentSecurityPolicy/iframe-srcdoc-import-bypass.html b/LayoutTests/http/tests/security/contentSecurityPolicy/iframe-srcdoc-import-bypass.html
new file mode 100644
index 000000000000..5d45317025b9
--- /dev/null
+++ b/LayoutTests/http/tests/security/contentSecurityPolicy/iframe-srcdoc-import-bypass.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval';">
+<script src="/js-test-resources/js-test.js"></script>
+</head>
+<body>
+<script>
+description("This test checks if iframes without an initial srcdoc attribute incorrectly bypass CSP when srcdoc is set later.");
+
+window.jsTestIsAsync = true;
+
+let resultsReceived = 0;
+let iframe1Loaded;
+let iframe2Loaded;
+
+function reportResult(iframeId, loaded) {
+ if (iframeId === "iframe1")
+ iframe1Loaded = loaded;
+ else
+ iframe2Loaded = loaded;
+ resultsReceived++;
+
+ if (resultsReceived === 2)
+ finishTest();
+}
+
+function finishTest() {
+ shouldBeFalse("iframe1Loaded");
+ shouldBeFalse("iframe2Loaded");
+ finishJSTest();
+}
+
+window.addEventListener("message", (event) => {
+ if (event.data.type === "import-result")
+ reportResult(event.data.iframe, event.data.loaded);
+});
+
+document.addEventListener("DOMContentLoaded", function() {
+ const iframe1 = document.getElementById("iframe1");
+ const iframe2 = document.getElementById("iframe2");
+ const srcdocContent = `
+ <script>
+ (async function() {
+ try {
+ await import('https://localhost:8443/security/contentSecurityPolicy/resources/module-pass.py');
+ window.parent.postMessage({type: 'import-result', iframe: '${iframe1.id}', loaded: true}, '*');
+ } catch (e) {
+ window.parent.postMessage({type: 'import-result', iframe: '${iframe1.id}', loaded: false}, '*');
+ }
+ })();
+ <\/script>
+ `;
+
+ iframe1.srcdoc = srcdocContent;
+ iframe2.srcdoc = srcdocContent.replaceAll(iframe1.id, iframe2.id);
+});
+</script>
+<iframe id="iframe1" sandbox="allow-scripts"></iframe>
+<iframe id="iframe2" sandbox="allow-scripts" srcdoc=""></iframe>
+</body>
+</html>
diff --git a/LayoutTests/http/tests/security/contentSecurityPolicy/resources/module-pass.py b/LayoutTests/http/tests/security/contentSecurityPolicy/resources/module-pass.py
new file mode 100755
index 000000000000..e9fe8b096842
--- /dev/null
+++ b/LayoutTests/http/tests/security/contentSecurityPolicy/resources/module-pass.py
@@ -0,0 +1,8 @@
+#!/usr/bin/env python3
+
+import sys
+
+sys.stdout.write(
+ "Access-Control-Allow-Origin: *\r\n"
+ "Content-Type: application/javascript\r\n\r\n"
+)
diff --git a/LayoutTests/imported/w3c/web-platform-tests/content-security-policy/inheritance/inheritance-from-initiator.sub-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/content-security-policy/inheritance/inheritance-from-initiator.sub-expected.txt
index aa86667a552d..7fe1e76a28a2 100644
--- a/LayoutTests/imported/w3c/web-platform-tests/content-security-policy/inheritance/inheritance-from-initiator.sub-expected.txt
+++ b/LayoutTests/imported/w3c/web-platform-tests/content-security-policy/inheritance/inheritance-from-initiator.sub-expected.txt
@@ -1,10 +1,10 @@
PASS Setting src inherits from parent.
-FAIL Changing contentWindow.location inherits from who changed it. assert_equals: expected "a" but got "p"
+PASS Changing contentWindow.location inherits from who changed it.
PASS Changing contentWindow.location indirectly inherits from who changed it directly.
-FAIL window.open() inherits from caller. assert_equals: expected "a" but got "p"
+PASS window.open() inherits from caller.
PASS Click on anchor inherits from owner of the anchor.
-FAIL Form submission through submit() inherits from owner of form. assert_equals: expected "b" but got "p"
-FAIL Form submission through button click inherits from owner of form. assert_equals: expected "b" but got "p"
+PASS Form submission through submit() inherits from owner of form.
+PASS Form submission through button click inherits from owner of form.
diff --git a/Source/WebCore/loader/DocumentWriter.cpp b/Source/WebCore/loader/DocumentWriter.cpp
index 582f47b89eb8..e0ba1bf687fc 100644
--- a/Source/WebCore/loader/DocumentWriter.cpp
+++ b/Source/WebCore/loader/DocumentWriter.cpp
@@ -228,7 +228,7 @@ bool DocumentWriter::begin(const URL& urlReference, bool dispatch, Document* own
return document->loader() && document->loader()->substituteData().isValid();
};
- if (currentHistoryItem && currentHistoryItem->policyContainer()) {
+ if (triggeringAction && triggeringAction->type() == NavigationType::BackForward && currentHistoryItem && currentHistoryItem->policyContainer()) {
const auto& policyContainerFromHistory = currentHistoryItem->policyContainer();
ASSERT(policyContainerFromHistory);
document->inheritPolicyContainerFrom(*policyContainerFromHistory);
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: A maliciously crafted webpage may be able to fingerprint the user
Researchers: Gongyu Ma (@Mezone0)
Key insight. A security helper that returned void swallowed its own failure: when a required sandbox extension could not be created the load proceeded anyway, and the resulting path-dependent behavioral difference was an observable side channel. Propagating the failure as a bool and hard-failing the load with blockedError removes the differential.
Patch Details
diff --git a/Source/WebKit/NetworkProcess/NetworkResourceLoadParameters.cpp b/Source/WebKit/NetworkProcess/NetworkResourceLoadParameters.cpp
index 3bc5c1e40bed..2206f06418ad 100644
--- a/Source/WebKit/NetworkProcess/NetworkResourceLoadParameters.cpp
+++ b/Source/WebKit/NetworkProcess/NetworkResourceLoadParameters.cpp
@@ -33,7 +33,7 @@
namespace WebKit {
using namespace WebCore;
-void NetworkResourceLoadParameters::createSandboxExtensionHandlesIfNecessary()
+bool NetworkResourceLoadParameters::createSandboxExtensionHandlesIfNecessary()
{
if (RefPtr httpBody = request.httpBody()) {
for (const FormDataElement& element : httpBody->elements()) {
@@ -57,7 +57,9 @@ void NetworkResourceLoadParameters::createSandboxExtensionHandlesIfNecessary()
if (auto handle = SandboxExtension::createHandle(request.url().fileSystemPath(), SandboxExtension::Type::ReadOnly))
resourceSandboxExtension = WTF::move(*handle);
}
+ return resourceSandboxExtension.has_value();
}
+ return true;
}
RefPtr<SecurityOrigin> NetworkResourceLoadParameters::parentOrigin() const
diff --git a/Source/WebKit/NetworkProcess/NetworkResourceLoadParameters.h b/Source/WebKit/NetworkProcess/NetworkResourceLoadParameters.h
index b30d39f610db..89339c55f05d 100644
--- a/Source/WebKit/NetworkProcess/NetworkResourceLoadParameters.h
+++ b/Source/WebKit/NetworkProcess/NetworkResourceLoadParameters.h
@@ -50,7 +50,7 @@ class Encoder;
namespace WebKit {
struct NetworkResourceLoadParameters {
- void createSandboxExtensionHandlesIfNecessary();
+ bool createSandboxExtensionHandlesIfNecessary();
RefPtr<WebCore::SecurityOrigin> NODELETE parentOrigin() const;
NetworkLoadParameters networkLoadParameters() const;
diff --git a/Source/WebKit/WebProcess/Network/WebLoaderStrategy.cpp b/Source/WebKit/WebProcess/Network/WebLoaderStrategy.cpp
index 0818b6397532..ef050f1bf94d 100644
--- a/Source/WebKit/WebProcess/Network/WebLoaderStrategy.cpp
+++ b/Source/WebKit/WebProcess/Network/WebLoaderStrategy.cpp
@@ -462,7 +462,12 @@ void WebLoaderStrategy::scheduleLoadFromNetworkProcess(ResourceLoader& resourceL
trackingParameters.frameID,
request
};
- loadParameters.createSandboxExtensionHandlesIfNecessary();
+ if (!loadParameters.createSandboxExtensionHandlesIfNecessary()) {
+ RunLoop::mainSingleton().dispatch([resourceLoader = Ref { resourceLoader }, error = blockedError(request)] {
+ resourceLoader->didFail(error);
+ });
+ return;
+ }
loadParameters.identifier = identifier;
loadParameters.parentPID = legacyPresentingApplicationPID();
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: Minse Kim, Narcis Oliveras Fontàs, Söhnke Benedikt Fischedick (Tripton), Daniel Rhea, Nathaniel Oh (@calysteon)
Key insight. The immutable TypedArray methods read the length and the source span in two separate steps, so a concurrent growable-SAB grow between them caused an out-of-bounds copy; snapshotting the span once makes length and source consistent.
Patch Details
diff --git a/JSTests/stress/growable-sharedarraybuffer-parallel-grow-during-prototype-methods.js b/JSTests/stress/growable-sharedarraybuffer-parallel-grow-during-prototype-methods.js
new file mode 100644
index 000000000000..77e099108dbb
--- /dev/null
+++ b/JSTests/stress/growable-sharedarraybuffer-parallel-grow-during-prototype-methods.js
@@ -0,0 +1,36 @@
+const ROUNDS = 10;
+const ITERATIONS_PER_ROUND = 5000;
+
+for (let round = 0; round < ROUNDS; round++) {
+ const N = 16;
+ const maxBytes = N * 4 + 4096;
+ const sab = new SharedArrayBuffer(N * 4, { maxByteLength: maxBytes });
+ const ta = new Int32Array(sab);
+
+ $.agent.start(`
+ $.agent.receiveBroadcast((sab, idx) => {
+ for (let i = 0; i < 50000; i++) {
+ try {
+ if (sab.byteLength < ${maxBytes}) {
+ sab.grow(sab.byteLength + 64);
+ }
+ } catch (e) {}
+ }
+ $.agent.report("done");
+ $.agent.leaving();
+ });
+ `);
+
+ $.agent.broadcast(sab, 0);
+
+ for (let i = 0; i < ITERATIONS_PER_ROUND; i++) {
+ try {
+ ta.with(0, 0x41414141);
+ ta.toReversed();
+ ta.toSorted();
+ } catch (e) {}
+ }
+
+ $.agent.sleep(50);
+ $.agent.getReport();
+}
diff --git a/Source/JavaScriptCore/runtime/JSGenericTypedArrayViewPrototypeFunctions.h b/Source/JavaScriptCore/runtime/JSGenericTypedArrayViewPrototypeFunctions.h
index 0351cbb12482..ab4126c46888 100644
--- a/Source/JavaScriptCore/runtime/JSGenericTypedArrayViewPrototypeFunctions.h
+++ b/Source/JavaScriptCore/runtime/JSGenericTypedArrayViewPrototypeFunctions.h
@@ -1636,19 +1636,19 @@ ALWAYS_INLINE EncodedJSValue genericTypedArrayViewProtoFuncToReversed(VM& vm, JS
validateTypedArray(globalObject, thisObject);
RETURN_IF_EXCEPTION(scope, { });
- size_t length = thisObject->length();
+ // Snapshot the span at this time, as SABs may grow (but never shrink) in parallel.
+ auto originalSpan = const_cast<const ViewClass*>(thisObject)->typedSpan();
+ size_t length = originalSpan.size();
bool isResizableOrGrowableShared = false;
Structure* structure = globalObject->typedArrayStructure(ViewClass::TypedArrayStorageType, isResizableOrGrowableShared);
ViewClass* result = ViewClass::createUninitialized(globalObject, structure, length);
RETURN_IF_EXCEPTION(scope, { });
- auto from = const_cast<const ViewClass*>(thisObject)->typedSpan();
- ASSERT(from.size() == length);
auto to = result->typedSpan();
ASSERT(to.size() == length);
- WTF::copyElements(to, from);
+ WTF::copyElements(to, originalSpan);
std::ranges::reverse(to);
return JSValue::encode(result);
@@ -1782,18 +1782,18 @@ ALWAYS_INLINE EncodedJSValue genericTypedArrayViewProtoFuncToSorted(VM& vm, JSGl
validateTypedArray(globalObject, thisObject);
RETURN_IF_EXCEPTION(scope, { });
- size_t length = thisObject->length();
+ // Snapshot the span at this time, as SABs may grow (but never shrink) in parallel.
+ auto originalSpan = const_cast<const ViewClass*>(thisObject)->typedSpan();
+ size_t length = originalSpan.size();
bool isResizableOrGrowableShared = false;
Structure* structure = globalObject->typedArrayStructure(ViewClass::TypedArrayStorageType, isResizableOrGrowableShared);
ViewClass* result = ViewClass::createUninitialized(globalObject, structure, length);
RETURN_IF_EXCEPTION(scope, { });
- auto from = const_cast<const ViewClass*>(thisObject)->typedSpan();
- ASSERT(from.size() == length);
auto to = result->typedSpan();
- WTF::copyElements(to, from);
+ WTF::copyElements(to, originalSpan);
RELEASE_AND_RETURN(scope, genericTypedArrayViewProtoFuncSortImpl(vm, globalObject, result, comparatorValue));
}
@@ -2108,12 +2108,13 @@ ALWAYS_INLINE EncodedJSValue genericTypedArrayViewProtoFuncWith(VM& vm, JSGlobal
ViewClass* result = ViewClass::createUninitialized(globalObject, structure, thisLength);
RETURN_IF_EXCEPTION(scope, { });
- size_t updatedLength = thisObject->length();
- if (thisLength != updatedLength) [[unlikely]] {
+ // Snapshot the span at this time, as SABs may grow (but never shrink) in parallel.
+ auto maybeUpdatedSpan = const_cast<const ViewClass*>(thisObject)->typedSpan();
+ if (thisLength != maybeUpdatedSpan.size()) [[unlikely]] {
// If TypedArray is shrunk, remaining part will be filled with NativeValue(undefined).
// But BigInt64Array / BigUint64Array throws a TypeError since undefined cannot be converted to BigInt.
if constexpr (ViewClass::Adaptor::isBigInt) {
- if (thisLength > updatedLength) [[unlikely]]
+ if (thisLength > maybeUpdatedSpan.size()) [[unlikely]]
return throwVMTypeError(globalObject, scope, "Cannot convert undefined to BigInt"_s);
}
@@ -2128,10 +2129,8 @@ ALWAYS_INLINE EncodedJSValue genericTypedArrayViewProtoFuncWith(VM& vm, JSGlobal
result->setIndexQuicklyToNativeValue(index, fromValue);
}
} else {
- auto from = const_cast<const ViewClass*>(thisObject)->typedSpan();
- ASSERT(from.size() == thisLength);
auto to = result->typedSpan();
- WTF::copyElements(to, from);
+ WTF::copyElements(to, maybeUpdatedSpan);
to[replaceIndex] = nativeValue;
}
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: A malicious website may be able to process restricted web content outside the sandbox
Researchers: greenbynox, Arni Hardarson, and an anonymous researcher
Key insight. One of two parallel IPC arrays (the glyphs) was consumed in place as a view into renderer memory while only the advances were copied; drawing then trusted an attacker-controlled span length/lifetime. Copying both into owned Vectors restores validated, consistent buffers.
Patch Details
diff --git a/Source/WebKit/GPUProcess/graphics/RemoteGraphicsContext.cpp b/Source/WebKit/GPUProcess/graphics/RemoteGraphicsContext.cpp
index 9a93fe3ee915..627b8b021edc 100644
--- a/Source/WebKit/GPUProcess/graphics/RemoteGraphicsContext.cpp
+++ b/Source/WebKit/GPUProcess/graphics/RemoteGraphicsContext.cpp
@@ -411,7 +411,9 @@ void RemoteGraphicsContext::drawGlyphs(RenderingResourceIdentifier fontIdentifie
{
RefPtr font = resourceCache().cachedFont(fontIdentifier);
MESSAGE_CHECK(font);
- context().drawGlyphs(*font, glyphsAdvances.span<0>(), Vector<GlyphBufferAdvance>(glyphsAdvances.span<1>()), localAnchor, fontSmoothingMode);
+ Vector<GlyphBufferGlyph, 128> glyphs { glyphsAdvances.span<0>() };
+ Vector<GlyphBufferAdvance, 128> advances { glyphsAdvances.span<1>() };
+ context().drawGlyphs(*font, glyphs.span(), advances.span(), localAnchor, fontSmoothingMode);
}
void RemoteGraphicsContext::drawImageBuffer(RenderingResourceIdentifier imageBufferIdentifier, const FloatRect& destinationRect, const FloatRect& srcRect, ImagePaintingOptions options)
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: A malicious website may be able to access script message handlers intended for other origins
Researchers: Hongze Wu and Shuaike Dong from Ant Group Infrastructure Security Team, and webb
Key insight. Script message handlers were dispatched based only on the descriptor’s existence, never checking that the caller’s origin matched the frame the handler belonged to, so a cross-origin frame reference could reach another origin’s handler; the fix adds an explicit same-origin check on both post paths.
Overview
Impact:
Description: Visiting a maliciously crafted website may lead to a cross-site scripting attack
Researchers: @hamayanhamayan
Key insight. The vulnerability is a classifier-divergence bug: two different predicates (live MediaPlayer::supportsType vs the static MIMETypeRegistry) disagreed about whether a MIME type is ‘media’, so a resource admitted as media could still be built as an executable document — HTML labeled as media got parsed and its script ran. Consistency of a security-relevant type decision across code paths is the real invariant.
Overview
Impact:
Description: Processing maliciously crafted web content may bypass Same Origin Policy
Researchers: Thomas Espach
Key insight. Navigation rewrite eligibility must require same-origin (and matching userinfo), not merely same-site; the same-site allowance let cross-origin subdomains be intercepted.
Patch Details
diff --git a/Source/WebCore/page/Navigation.cpp b/Source/WebCore/page/Navigation.cpp
index 5ec713b9e73b..bc56932bbe11 100644
--- a/Source/WebCore/page/Navigation.cpp
+++ b/Source/WebCore/page/Navigation.cpp
@@ -831,12 +831,11 @@ static bool documentCanHaveURLRewritten(const Document& document, const URL& tar
const URL& documentURL = document.url();
Ref documentOrigin = document.securityOrigin();
auto targetOrigin = SecurityOrigin::create(targetURL);
- bool isSameSite = documentOrigin->isSameSiteAs(targetOrigin);
- bool isSameOrigin = documentOrigin->isSameOriginAs(targetOrigin);
- // For cross-window navigation with document.domain, we need to check same-origin rather than same-site
- // to account for document.domain modifications that make cross-origin windows same-origin-domain
- if (!isSameSite && !isSameOrigin)
+ if (!documentOrigin->isSameOriginAs(targetOrigin))
+ return false;
+
+ if (documentURL.user() != targetURL.user() || documentURL.password() != targetURL.password())
return false;
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#can-have-its-url-rewritten
diff --git a/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/NavigationAPI.mm b/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/NavigationAPI.mm
index f3ca187b37f6..c68902414a4c 100644
--- a/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/NavigationAPI.mm
+++ b/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/NavigationAPI.mm
@@ -121,4 +121,74 @@ TEST(NavigationAPI, ReplaceStateAfterBackPreservesKey)
EXPECT_TRUE([keyPageA isEqualToString:keyAfterReplace]);
}
+TEST(NavigationAPI, InterceptFailsForDifferentSubdomain)
+{
+ HTTPServer server({
+ { "/page1"_s, { "page1"_s } },
+ { "/page2"_s, { "page2"_s } }
+ }, HTTPServer::Protocol::HttpsProxy);
+
+ RetainPtr configuration = server.httpsProxyConfiguration();
+ RetainPtr webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 300, 300) configuration:configuration.get()]);
+
+ RetainPtr navigationDelegate = adoptNS([TestNavigationDelegate new]);
+ [navigationDelegate allowAnyTLSCertificate];
+ [webView setNavigationDelegate:navigationDelegate.get()];
+
+ [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://page1.example.com/page1"]]];
+ [navigationDelegate waitForDidFinishNavigation];
+
+ NSError *error = nil;
+ id result = [webView objectByCallingAsyncFunction:@"return await new Promise((resolve) => {"
+ " navigation.addEventListener('navigate', (event) => {"
+ " try {"
+ " event.intercept({ handler: () => {} });"
+ " resolve('intercept succeeded');"
+ " } catch (e) {"
+ " resolve('intercept failed: ' + e.name);"
+ " }"
+ " });"
+ " location.href = 'https://page2.example.com/page2';"
+ "});" withArguments:nil error:&error];
+
+ EXPECT_NULL(error);
+ EXPECT_TRUE([result isKindOfClass:[NSString class]]);
+ EXPECT_TRUE([result hasPrefix:@"intercept failed"]);
+}
+
+TEST(NavigationAPI, InterceptFailsForDifferentUsernameAndPassword)
+{
+ HTTPServer server({
+ { "/page1"_s, { "page1"_s } },
+ { "/page2"_s, { "page2"_s } }
+ }, HTTPServer::Protocol::HttpsProxy);
+
+ RetainPtr configuration = server.httpsProxyConfiguration();
+ RetainPtr webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 300, 300) configuration:configuration.get()]);
+
+ RetainPtr navigationDelegate = adoptNS([TestNavigationDelegate new]);
+ [navigationDelegate allowAnyTLSCertificate];
+ [webView setNavigationDelegate:navigationDelegate.get()];
+
+ [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://username1:[email protected]/page1"]]];
+ [navigationDelegate waitForDidFinishNavigation];
+
+ NSError *error = nil;
+ id result = [webView objectByCallingAsyncFunction:@"return await new Promise((resolve) => {"
+ " navigation.addEventListener('navigate', (event) => {"
+ " try {"
+ " event.intercept({ handler: () => {} });"
+ " resolve('intercept succeeded');"
+ " } catch (e) {"
+ " resolve('intercept failed: ' + e.name);"
+ " }"
+ " });"
+ " location.href = 'https://username2:[email protected]/page2';"
+ "});" withArguments:nil error:&error];
+
+ EXPECT_NULL(error);
+ EXPECT_TRUE([result isKindOfClass:[NSString class]]);
+ EXPECT_TRUE([result hasPrefix:@"intercept failed"]);
+}
+
} // namespace TestWebKitAPI
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: HanQing from TSDubhe and Nan Wang (@eternalsakura13)
Key insight. Private-field slots share the PropertyTable with ordinary properties but must be exempt from user-triggered seal/freeze semantics; treating them like normal properties marks engine-managed storage ReadOnly/DontDelete and breaks the private-field access invariant that such slots are always writable.
Patch Details
diff --git a/JSTests/stress/private-names-seal-freeze.js b/JSTests/stress/private-names-seal-freeze.js
new file mode 100644
index 000000000000..6849fa1490df
--- /dev/null
+++ b/JSTests/stress/private-names-seal-freeze.js
@@ -0,0 +1,18 @@
+class C {
+ #field;
+ setField(v) {
+ this.#field = v;
+ }
+}
+
+for (let i = 0; i < 10000; i++) {
+ let c = new C();
+ Object.seal(c);
+ c.setField(i);
+}
+
+for (let i = 0; i < 10000; i++) {
+ let c = new C();
+ Object.freeze(c);
+ c.setField(i);
+}
diff --git a/Source/JavaScriptCore/runtime/PropertyTable.cpp b/Source/JavaScriptCore/runtime/PropertyTable.cpp
index 1cda3936aa91..6de4a5f291db 100644
--- a/Source/JavaScriptCore/runtime/PropertyTable.cpp
+++ b/Source/JavaScriptCore/runtime/PropertyTable.cpp
@@ -159,7 +159,8 @@ PropertyTable::~PropertyTable()
void PropertyTable::seal()
{
forEachPropertyMutable([&](auto& entry) {
- entry.setAttributes(entry.attributes() | static_cast<unsigned>(PropertyAttribute::DontDelete));
+ if (!PropertyName(entry.key()).isPrivateName())
+ entry.setAttributes(entry.attributes() | static_cast<unsigned>(PropertyAttribute::DontDelete));
return IterationStatus::Continue;
});
}
@@ -167,10 +168,12 @@ void PropertyTable::seal()
void PropertyTable::freeze()
{
forEachPropertyMutable([&](auto& entry) {
- if (!(entry.attributes() & PropertyAttribute::Accessor))
- entry.setAttributes(entry.attributes() | static_cast<unsigned>(PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly));
- else
- entry.setAttributes(entry.attributes() | static_cast<unsigned>(PropertyAttribute::DontDelete));
+ if (!PropertyName(entry.key()).isPrivateName()) {
+ if (!(entry.attributes() & PropertyAttribute::Accessor))
+ entry.setAttributes(entry.attributes() | static_cast<unsigned>(PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly));
+ else
+ entry.setAttributes(entry.attributes() | static_cast<unsigned>(PropertyAttribute::DontDelete));
+ }
return IterationStatus::Continue;
});
}
@@ -179,7 +182,7 @@ bool PropertyTable::isSealed() const
{
bool result = true;
forEachProperty([&](const auto& entry) {
- if ((entry.attributes() & PropertyAttribute::DontDelete) != static_cast<unsigned>(PropertyAttribute::DontDelete)) {
+ if (!PropertyName(entry.key()).isPrivateName() && (entry.attributes() & PropertyAttribute::DontDelete) != static_cast<unsigned>(PropertyAttribute::DontDelete)) {
result = false;
return IterationStatus::Done;
}
@@ -192,13 +195,15 @@ bool PropertyTable::isFrozen() const
{
bool result = true;
forEachProperty([&](const auto& entry) {
- if (!(entry.attributes() & PropertyAttribute::DontDelete)) {
- result = false;
- return IterationStatus::Done;
- }
- if (!(entry.attributes() & (PropertyAttribute::ReadOnly | PropertyAttribute::Accessor))) {
- result = false;
- return IterationStatus::Done;
+ if (!PropertyName(entry.key()).isPrivateName()) {
+ if (!(entry.attributes() & PropertyAttribute::DontDelete)) {
+ result = false;
+ return IterationStatus::Done;
+ }
+ if (!(entry.attributes() & (PropertyAttribute::ReadOnly | PropertyAttribute::Accessor))) {
+ result = false;
+ return IterationStatus::Done;
+ }
}
return IterationStatus::Continue;
});
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: EntryHi
Background
Advisory: Apple Security Advisory
| Attribute | Value |
|---|---|
| CVE | CVE-2026-20635 |
| Bugzilla | 304661 |
| Component | WebKit |
| Bug Class | LogicError |
| Severity | medium |
| Advisory | Apple Advisory |
Fix not public. The WebKit fix for this bug is embargoed or not yet disclosed. The bug ID is confirmed from the Apple advisory, but no public commit references it.
Root Cause Analysis
The issue was addressed with improved memory handling.
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: EntryHi
Background
Advisory: Apple Security Advisory
| Attribute | Value |
|---|---|
| CVE | CVE-2026-20636 |
| Bugzilla | 304657 |
| Component | WebKit |
| Bug Class | LogicError |
| Severity | medium |
| Advisory | Apple Advisory |
Fix not public. The WebKit fix for this bug is embargoed or not yet disclosed. The bug ID is confirmed from the Apple advisory, but no public commit references it.
Root Cause Analysis
The issue was addressed with improved memory handling.
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: HanQing from TSDubhe and Nan Wang (@eternalsakura13)
Key insight. The bug is an architecture-portability defect in the Wasm tail-call prologue: scratch-register spill/frame-clobber logic written for x86_64’s aliasing scratch register was applied unchanged on other CPUs that need a frame restore and different guarantees instead, and the fix cleanly separates the x86_64 and non-x86_64 code paths and adds a proper emitRestore plus aliasing assertions.
Overview
Impact:
Description: A remote attacker may be able to cause a denial-of-service
Researchers: Nathaniel Oh (@calysteon)
Key insight. A classic unsigned-underflow-to-huge-length bug: an unchecked ‘a - b’ on size_t buffer indices assumed a >= b, and violating that assumption produced a near-SIZE_MAX span length driving an out-of-bounds read. The fix is the canonical WTF::safeSub guard with an empty-result early return.
Patch Details
diff --git a/Source/WebCore/platform/audio/AudioResamplerKernel.cpp b/Source/WebCore/platform/audio/AudioResamplerKernel.cpp
index 4d64f8ff81c3..e5bcb5e1f613 100644
--- a/Source/WebCore/platform/audio/AudioResamplerKernel.cpp
+++ b/Source/WebCore/platform/audio/AudioResamplerKernel.cpp
@@ -31,6 +31,7 @@
#include "AudioResampler.h"
#include "AudioUtilities.h"
#include <algorithm>
+#include <wtf/CheckedArithmetic.h>
#include <wtf/TZoneMallocInlines.h>
namespace WebCore {
@@ -58,7 +59,10 @@ std::span<float> AudioResamplerKernel::getSourceSpan(size_t framesToProcess, siz
// Determine how many input frames we'll need.
// We need to fill the buffer up to and including endIndex (so add 1) but we've already buffered m_fillIndex frames from last time.
- size_t framesNeeded = 1 + endIndex - m_fillIndex;
+ size_t framesNeeded;
+ if (!WTF::safeSub(1 + endIndex, m_fillIndex, framesNeeded))
+ return { };
+
if (numberOfSourceFramesNeededP)
*numberOfSourceFramesNeededP = framesNeeded;
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: A website may be able to track users through Safari web extensions
Researchers: Tom Van Goethem
Key insight. A privacy guarantee (URL masking for extension-injected content) was enforced in the HTML serialization path but not in the parallel XMLSerializer path, so an alternate serialization API leaked exactly the data the masking scheme was designed to hide — a classic ‘one API hardened, a sibling API forgotten’ consistency gap.
Overview
Impact:
Description: Processing maliciously crafted web content may lead to memory corruption. Apple is aware of a report that this issue may have been exploited in an extremely sophisticated attack against specific targeted individuals on versions of iOS before iOS 26. CVE-2025-43529 was also issued in response to this report.
Researchers: Apple and Google Threat Analysis Group
Background
Advisory: Apple Security Advisory
| Attribute | Value |
|---|---|
| CVE | CVE-2025-14174 |
| Bugzilla | 303614 |
| Component | WebKit |
| Bug Class | LogicError |
| Severity | medium |
| Advisory | Apple Advisory |
Fix not public. The WebKit fix for this bug is embargoed or not yet disclosed. The bug ID is confirmed from the Apple advisory, but no public commit references it.
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: Hossein Lotfi (@hosselot) of Trend Micro Zero Day Initiative
Key insight. HashTable sized itself with plain unsigned arithmetic, so an enormous key count could overflow the power-of-two/doubling math and allocate a table far too small; Checked<unsigned> makes the overflow trap.
Patch Details
diff --git a/Source/WTF/wtf/HashTable.h b/Source/WTF/wtf/HashTable.h
index 20525b041e77..d8bac5608901 100644
--- a/Source/WTF/wtf/HashTable.h
+++ b/Source/WTF/wtf/HashTable.h
@@ -31,6 +31,7 @@
#include <utility>
#include <wtf/AlignedStorage.h>
#include <wtf/Assertions.h>
+#include <wtf/CheckedArithmetic.h>
#include <wtf/DebugHeap.h>
#include <wtf/FastMalloc.h>
#include <wtf/HashTraits.h>
@@ -570,6 +571,7 @@ DECLARE_ALLOCATOR_WITH_HEAP_IDENTIFIER(HashTable);
void remove(ValueType*);
void deleteWeakNullEntries();
+ void deleteReleasedWeakBuckets();
static constexpr unsigned computeBestTableSize(unsigned keyCount);
bool shouldExpand() const { return HashTableSizePolicy::shouldExpand(keyCount() + deletedCount(), tableSize()); }
@@ -579,7 +581,7 @@ DECLARE_ALLOCATOR_WITH_HEAP_IDENTIFIER(HashTable);
void shrink() { rehash(tableSize() / 2, nullptr); }
void shrinkToBestSize();
- ValueType* rehash(unsigned newTableSize, ValueType* entry);
+ ValueType* rehash(Checked<unsigned> newTableSize, ValueType* entry);
ValueType* reinsert(ValueType&&);
static void initializeBucket(ValueType& bucket);
@@ -1242,8 +1244,8 @@ DECLARE_ALLOCATOR_WITH_HEAP_IDENTIFIER(HashTable);
if constexpr (KeyTraits::hasIsWeakNullValueFunction)
deleteWeakNullEntries();
- unsigned newSize;
- unsigned oldSize = tableSize();
+ Checked<unsigned> newSize;
+ Checked<unsigned> oldSize = tableSize();
if (!oldSize)
newSize = KeyTraits::minimumTableSize;
else if (mustRehashInPlace())
@@ -1257,7 +1259,7 @@ DECLARE_ALLOCATOR_WITH_HEAP_IDENTIFIER(HashTable);
template<typename Key, typename Value, typename Extractor, typename HashFunctions, typename Traits, typename KeyTraits, typename Malloc>
constexpr unsigned HashTable<Key, Value, Extractor, HashFunctions, Traits, KeyTraits, Malloc>::computeBestTableSize(unsigned keyCount)
{
- unsigned bestTableSize = roundUpToPowerOfTwo(keyCount);
+ Checked<unsigned> bestTableSize = roundUpToPowerOfTwo(keyCount);
constexpr double minLoadRatio = 1.0 / minLoad;
if (HashTableSizePolicy::shouldExpand(keyCount, bestTableSize))
@@ -1285,7 +1287,7 @@ DECLARE_ALLOCATOR_WITH_HEAP_IDENTIFIER(HashTable);
if (aboveThresholdForEagerExpansion(largeLoadFactor, keyCount, bestTableSize))
bestTableSize *= 2;
}
- return std::max(bestTableSize, static_cast<unsigned>(KeyTraits::minimumTableSize));
+ return std::max(bestTableSize.value(), KeyTraits::minimumTableSize);
}
template<typename Key, typename Value, typename Extractor, typename HashFunctions, typename Traits, typename KeyTraits, typename Malloc>
@@ -1295,7 +1297,21 @@ DECLARE_ALLOCATOR_WITH_HEAP_IDENTIFIER(HashTable);
}
template<typename Key, typename Value, typename Extractor, typename HashFunctions, typename Traits, typename KeyTraits, typename Malloc>
- auto HashTable<Key, Value, Extractor, HashFunctions, Traits, KeyTraits, Malloc>::rehash(unsigned newTableSize, ValueType* entry) -> ValueType*
+ void HashTable<Key, Value, Extractor, HashFunctions, Traits, KeyTraits, Malloc>::deleteReleasedWeakBuckets()
+ {
+ unsigned tableSize = this->tableSize();
+ for (unsigned i = 0; i < tableSize; ++i) {
+ auto& entry = m_table[i];
+ if (isReleasedWeakBucket(entry)) {
+ deleteBucket(entry);
+ setDeletedCount(deletedCount() + 1);
+ setKeyCount(keyCount() - 1);
+ }
+ }
+ }
+
+ template<typename Key, typename Value, typename Extractor, typename HashFunctions, typename Traits, typename KeyTraits, typename Malloc>
+ auto HashTable<Key, Value, Extractor, HashFunctions, Traits, KeyTraits, Malloc>::rehash(Checked<unsigned> newTableSize, ValueType* entry) -> ValueType*
{
internalCheckTableConsistencyExceptSize();
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to arbitrary code execution. Apple is aware of a report that this issue may have been exploited in an extremely sophisticated attack against specific targeted individuals on versions of iOS before iOS 26. CVE-2025-14174 was also issued in response to this report.
Researchers: Google Threat Analysis Group
Key insight. In whole-procedure mode the store-barrier phase reset only the Phi node and not its transitive incoming values, so an allocation that escaped through a Phi could lose its required write barrier — a GC-invariant violation that becomes a use-after-free.
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: Phil Pizlo of Epic Games
Key insight. The lock-free medium-directory lookup returned a raw tuple pointer that callers dereferenced after the guarded read, so a concurrent scavenger/reallocation could free it; capturing the needed fields by value within the dependency-checked read removes the race.
Patch Details
diff --git a/Source/bmalloc/libpas/src/libpas/pas_local_allocator_inlines.h b/Source/bmalloc/libpas/src/libpas/pas_local_allocator_inlines.h
index cb70a8ede00c..48c4c4d51b65 100644
--- a/Source/bmalloc/libpas/src/libpas/pas_local_allocator_inlines.h
+++ b/Source/bmalloc/libpas/src/libpas/pas_local_allocator_inlines.h
@@ -1833,6 +1833,7 @@ pas_local_allocator_try_allocate(pas_local_allocator* allocator,
pas_allocation_result result;
PAS_TESTING_ASSERT(!allocator->scavenger_data.is_in_use);
+ PAS_TESTING_ASSERT(size <= allocator->object_size || !allocator->object_size);
if (verbose) {
pas_log("Allocator %p (%s) allocating size = %zu, alignment = %zu.\n",
@@ -1854,6 +1855,7 @@ pas_local_allocator_try_allocate(pas_local_allocator* allocator,
(void*)result.begin);
}
+ PAS_TESTING_ASSERT(size <= allocator->object_size);
return result_filter(
pas_allocation_result_create_success_with_zero_mode(result.begin, result.zero_mode));
}
@@ -1875,6 +1877,7 @@ pas_local_allocator_try_allocate(pas_local_allocator* allocator,
allocator, allocation_mode, counts, result_filter);
if (verbose)
pas_log("in small segregated slow return - result.begin = %p\n", (void*)result.begin);
+ PAS_TESTING_ASSERT(size <= allocator->object_size);
return result;
}
@@ -1882,6 +1885,7 @@ pas_local_allocator_try_allocate(pas_local_allocator* allocator,
allocator, size, alignment, allocation_mode, counts, result_filter);
if (verbose)
pas_log("in generic return - result.begin = %p\n", (void*)result.begin);
+ PAS_TESTING_ASSERT(size <= allocator->object_size);
return result;
}
diff --git a/Source/bmalloc/libpas/src/libpas/pas_segregated_heap.c b/Source/bmalloc/libpas/src/libpas/pas_segregated_heap.c
index b46f8ac7aca6..9d7fcbf3a977 100644
--- a/Source/bmalloc/libpas/src/libpas/pas_segregated_heap.c
+++ b/Source/bmalloc/libpas/src/libpas/pas_segregated_heap.c
@@ -344,7 +344,7 @@ pas_segregated_size_directory* pas_segregated_heap_size_directory_for_index_slow
}
typedef struct {
- pas_segregated_heap_medium_directory_tuple* tuple;
+ pas_segregated_heap_medium_directory_result value;
uintptr_t dependency;
} medium_directory_tuple_for_index_impl_result;
@@ -360,7 +360,7 @@ medium_directory_tuple_for_index_impl(
unsigned begin;
unsigned end;
- pas_segregated_heap_medium_directory_tuple* best;
+ pas_segregated_heap_medium_directory_result best;
medium_directory_tuple_for_index_impl_result result;
PAS_ASSERT(rare_data);
@@ -370,7 +370,7 @@ medium_directory_tuple_for_index_impl(
begin = 0;
end = num_medium_directories;
- best = NULL;
+ best = pas_segregated_heap_medium_directory_result_create_empty();
result.dependency = (uintptr_t)medium_directories;
@@ -379,6 +379,7 @@ medium_directory_tuple_for_index_impl(
pas_segregated_heap_medium_directory_tuple* directory;
unsigned begin_index;
unsigned end_index;
+ pas_segregated_heap_medium_directory_result result_for_current;
middle = (begin + end) >> 1;
@@ -395,7 +396,7 @@ medium_directory_tuple_for_index_impl(
or the tuple straddling page boundary, leading to the begin index being zero and the end_index
having its original value. */
if (!begin_index) {
- result.tuple = NULL;
+ result.value = pas_segregated_heap_medium_directory_result_create_empty();
return result;
}
@@ -403,9 +404,18 @@ medium_directory_tuple_for_index_impl(
result.dependency += begin_index + end_index;
+ result_for_current.tuple_unsafe_without_lock = directory;
+ result_for_current.directory =
+ pas_compact_atomic_segregated_size_directory_ptr_load(&directory->directory);
+ result_for_current.allocator_index = directory->allocator_index;
+
+ /* Don't include the tuple in the dependency since you should only be using it if you're
+ holding the lock. */
+ result.dependency +=
+ (uintptr_t)result_for_current.directory + result_for_current.allocator_index;
if (index < begin_index) {
end = middle;
- best = directory;
+ best = result_for_current;
continue;
}
@@ -414,33 +424,33 @@ medium_directory_tuple_for_index_impl(
continue;
}
- result.tuple = directory;
+ result.value = result_for_current;
return result;
}
switch (search_mode) {
case pas_segregated_heap_medium_size_directory_search_within_size_class_progression:
- result.tuple = NULL;
+ result.value = pas_segregated_heap_medium_directory_result_create_empty();
return result;
case pas_segregated_heap_medium_size_directory_search_least_greater_equal:
- result.tuple = best;
+ result.value = best;
return result;
}
PAS_ASSERT_NOT_REACHED();
- result.tuple = NULL;
+ result.value = pas_segregated_heap_medium_directory_result_create_empty();
return result;
}
-static pas_segregated_heap_medium_directory_tuple*
+static pas_segregated_heap_medium_directory_result
medium_directory_tuple_for_index_with_lock(
pas_segregated_heap* heap,
size_t index,
pas_segregated_heap_medium_size_directory_search_mode search_mode,
pas_lock_hold_mode heap_lock_hold_mode)
{
- pas_segregated_heap_medium_directory_tuple* result;
+ pas_segregated_heap_medium_directory_result result;
pas_segregated_heap_rare_data* rare_data;
pas_segregated_heap_medium_directory_tuple* medium_directories;
@@ -455,14 +465,14 @@ medium_directory_tuple_for_index_with_lock(
medium_directories,
rare_data->num_medium_directories,
index,
- search_mode).tuple;
+ search_mode).value;
pas_heap_lock_unlock_conditionally(heap_lock_hold_mode);
return result;
}
-pas_segregated_heap_medium_directory_tuple*
+pas_segregated_heap_medium_directory_result
pas_segregated_heap_medium_directory_tuple_for_index(
pas_segregated_heap* heap,
size_t index,
@@ -479,7 +489,7 @@ pas_segregated_heap_medium_directory_tuple_for_index(
rare_data = pas_segregated_heap_rare_data_ptr_load(&heap->rare_data);
if (!rare_data)
- return NULL;
+ return pas_segregated_heap_medium_directory_result_create_empty();
if (heap_lock_hold_mode == pas_lock_is_held) {
return medium_directory_tuple_for_index_with_lock(
@@ -502,9 +512,9 @@ pas_segregated_heap_medium_directory_tuple_for_index(
if (pas_mutation_count_matches_with_dependency(
&rare_data->mutation_count, saved_count, result.dependency)) {
- if (verbose && !result.tuple)
+ if (verbose && !result.value.directory)
pas_log("did not find tuple\n");
- return result.tuple;
+ return result.value;
}
return medium_directory_tuple_for_index_with_lock(
@@ -519,20 +529,16 @@ unsigned pas_segregated_heap_medium_allocator_index_for_index(
{
static const bool verbose = PAS_SHOULD_LOG(PAS_LOG_SEGREGATED_HEAPS);
- pas_segregated_heap_medium_directory_tuple* medium_directory;
+ pas_segregated_heap_medium_directory_result medium_directory;
+ unsigned result;
medium_directory = pas_segregated_heap_medium_directory_tuple_for_index(
heap, index, search_mode, heap_lock_hold_mode);
- if (medium_directory) {
- unsigned result;
- result = medium_directory->allocator_index;
- if (verbose && !result)
- pas_log("found null allocator index\n");
- return result;
- }
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: Google Big Sleep, Nan Wang (@eternalsakura13)
Key insight. BBQ-JIT addArraySet used the index operand but never consume()d its register, leaking a binding and corrupting the allocator’s register model; adding consume(index) fixes the miscompile.
Patch Details
diff --git a/JSTests/wasm/stress/bbq-array-set-consume.js b/JSTests/wasm/stress/bbq-array-set-consume.js
new file mode 100644
index 000000000000..28c082dca919
--- /dev/null
+++ b/JSTests/wasm/stress/bbq-array-set-consume.js
@@ -0,0 +1,13 @@
+ function sakura_main() {
+ var wasm_code = new Uint8Array([
+ 0,97,115,109,1,0,0,0,1,183,128,128,128,0,10,78,1,94,120,1,78,1,94,119,1,78,1,80,0,95,0,78,1,96,3,127,127,127,1,127,96,0,0,96,0,1,127,96,1,127,1,127,96,0,1,125,96,0,1,124,96,5,108,127,127,127,127,1,124,3,130,128,128,128,0,1,3,4,133,128,128,128,0,1,112,1,1,1,5,131,128,128,128,0,1,0,123,13,131,128,128,128,0,1,0,4,6,253,134,128,128,0,39,127,1,65,223,155,138,142,125,65,143,193,209,238,2,65,142,221,236,208,126,108,107,11,100,2,1,251,0,2,11,100,109,0,65,148,129,129,222,2,251,28,11,127,1,65,160,220,185,195,0,65,157,149,131,223,120,107,65,131,226,177,36,65,168,185,207,158,4,65,163,211,217,165,6,65,231,220,176,191,127,106,107,65,207,247,142,202,4,65,244,129,173,200,1,107,65,129,134,211,145,3,65,177,216,161,189,7,107,108,106,108,108,11,100,110,1,65,163,229,198,186,127,65,219,182,158,140,7,65,200,236,218,246,1,65,154,187,143,229,121,106,65,173,243,249,167,3,65,219,157,223,3,65,159,191,179,185,125,65,165,149,210,130,4,106,108,65,227,236,163,194,124,108,65,247,223,242,157,122,107,65,241,224,201,165,2,65,161,254,234,231,7,65,221,215,211,255,121,65,140,188,128,196,4,107,65,154,197,237,251,2,65,151,153,169,187,122,65,141,212,212,227,126,108,65,175,176,250,169,127,108,106,65,197,141,244,188,5,65,198,192,252,213,4,106,65,131,233,179,191,122,107,65,159,166,204,206,4,108,108,65,175,225,164,232,121,65,237,161,155,138,127,65,148,244,241,4,65,155,188,128,185,7,106,107,65,232,135,186,138,122,65,184,203,175,153,127,107,65,204,223,173,254,1,65,200,193,250,159,4,65,215,214,218,142,121,65,222,203,131,246,1,107,65,153,153,177,233,120,65,130,181,193,212,123,108,108,107,65,155,152,231,215,6,65,186,135,251,133,125,107,107,65,171,218,139,154,121,65,195,165,251,166,4,65,216,152,175,229,121,106,65,174,202,198,194,122,65,172,169,175,183,121,65,194,155,237,202,0,65,131,220,225,242,2,107,65,168,164,225,140,126,65,226,252,175,216,120,108,108,107,65,154,248,189,180,127,107,65,209,254,176,170,7,65,134,177,231,194,124,106,108,108,65,139,243,178,150,1,108,65,151,171,242,167,3,65,197,225,200,212,1,65,198,166,172,159,120,65,140,189,228,132,4,65,164,138,252,132,3,65,204,218,128,245,122,108,65,208,222,165,216,122,65,146,192,157,140,125,65,217,194,152,212,6,108,65,184,133,225,152,126,107,65,234,239,146,242,4,106,107,65,225,148,144,133,3,107,107,65,149,235,189,151,5,107,108,65,178,178,204,214,120,108,65,172,226,205,198,4,107,65,240,154,179,225,6,107,107,65,160,160,166,139,2,107,65,133,164,208,71,106,65,194,230,143,238,126,108,65,223,200,226,165,120,65,210,216,129,249,7,107,106,107,106,65,210,194,205,149,6,107,65,177,129,211,71,65,170,132,137,153,3,65,139,140,195,254,124,107,107,108,107,65,196,170,201,200,122,108,107,108,108,65,0,106,65,0,107,65,0,108,65,0,108,106,65,0,108,65,0,108,65,0,107,108,106,108,108,65,0,107,107,65,0,108,108,65,0,107,65,0,107,108,65,0,108,65,0,107,65,0,107,108,65,0,107,65,0,106,65,0,106,106,65,0,107,65,0,107,107,108,106,65,0,106,65,0,106,65,0,107,65,0,108,65,0,65,0,65,0,65,0,65,0,65,0,251,8,0,7,11,127,0,65,0,11,127,0,65,0,11,127,0,65,0,11,127,0,65,0,11,127,0,65,0,11,127,0,65,0,11,127,0,65,0,11,127,0,65,0,11,127,0,65,0,11,127,0,65,0,11,127,0,65,0,11,127,0,65,0,11,127,0,65,0,11,127,0,65,0,11,127,0,65,0,11,127,0,65,0,11,127,0,65,0,11,127,0,65,0,11,127,0,65,0,11,127,0,65,0,11,127,0,65,0,11,127,0,65,0,11,127,0,65,0,11,127,0,65,0,11,127,0,65,0,11,127,0,65,0,11,127,0,65,0,11,127,0,65,0,11,127,0,65,0,11,127,0,65,0,11,127,0,65,0,11,127,0,65,0,11,127,0,65,0,11,127,0,65,0,11,7,136,128,128,128,0,1,4,109,97,105,110,0,0,9,139,128,128,128,0,1,6,0,65,0,11,112,1,210,0,11,10,233,131,128,128,0,1,230,3,7,1,127,1,100,111,1,125,2,127,1,114,1,127,1,100,107,208,111,212,33,4,2,127,2,105,2,105,2,105,2,105,2,105,31,5,5,1,0,4,3,3,1,0,2,3,1,1,0,0,65,243,57,63,0,79,65,175,152,174,204,7,66,228,196,170,233,213,187,202,140,213,0,62,0,0,67,127,25,165,73,65,180,204,148,31,26,252,5,65,0,253,15,65,0,253,15,253,209,1,253,22,12,40,0,156,38,65,255,141,49,65,227,132,32,16,0,1,36,0,254,57,2,210,147,2,67,202,31,95,107,145,143,67,197,207,144,69,91,65,169,7,65,228,187,25,72,65,146,226,225,132,5,208,112,251,23,3,20,3,172,81,2,6,1,11,11,12,5,11,26,65,17,12,4,11,26,65,231,254,207,0,12,3,11,26,65,31,12,2,11,26,65,179,241,164,242,0,12,1,11,26,65,244,216,21,12,0,11,2,125,31,7,0,2,124,2,105,2,64,2,105,2,105,2,64,31,8,5,3,4,2,3,1,0,2,3,1,0,0,0,2,124,68,102,47,219,170,228,129,161,74,11,11,12,5,11,68,155,170,229,144,164,245,180,78,12,4,11,26,68,50,10,67,205,35,36,157,244,12,3,11,26,68,249,13,67,48,14,244,80,138,12,2,11,208,108,65,167,222,232,0,65,181,196,221,156,1,65,211,233,229,0,65,191,131,3,2,9,33,3,33,3,33,3,33,3,26,68,225,165,33,2,164,202,66,219,11,12,1,11,26,68,90,79,126,46,106,22,193,112,12,0,11,182,11,12,0,11,169,4,126,66,184,246,191,163,230,139,128,177,123,5,66,217,151,211,160,129,186,180,162,82,11,208,1,65,193,6,65,224,216,45,254,39,0,196,220,2,65,192,199,237,73,251,14,1,66,217,233,182,250,128,195,239,183,206,0,66,75,136,137,65,161,176,14,65,155,248,186,6,65,0,254,46,0,0,79,254,21,1,163,222,2,89,115,251,28,251,23,106,212,251,23,110,251,23,2,212,251,23,110,251,23,107,212,251,23,106,251,23,0,212,251,23,1,251,22,107,33,10,65,177,198,6,11
+ ]);
+ var wasm_module = new WebAssembly.Module(wasm_code);
+ var wasm_instance = new WebAssembly.Instance(wasm_module);
+ var f = wasm_instance.exports.main;
+ f();
+ }
+
+try {
+ sakura_main();
+} catch { }
diff --git a/Source/JavaScriptCore/wasm/WasmBBQJIT64.cpp b/Source/JavaScriptCore/wasm/WasmBBQJIT64.cpp
index 97fd2af1d36d..50ae0204cf0c 100644
--- a/Source/JavaScriptCore/wasm/WasmBBQJIT64.cpp
+++ b/Source/JavaScriptCore/wasm/WasmBBQJIT64.cpp
@@ -1837,6 +1837,7 @@ WARN_UNUSED_RETURN PartialResult BBQJIT::addArraySet(uint32_t typeIndex, TypedEx
ASSERT(arrayref.asI64() == JSValue::encode(jsNull()));
LOG_INSTRUCTION("ArraySet", typeIndex, arrayref, index, value);
+ consume(index);
consume(value);
emitThrowException(ExceptionType::NullAccess);
return { };
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: Nan Wang (@eternalsakura13)
Key insight. Jettison was resetting each inline-cache stub — re-entering CodeBlock/structure state that is already being destroyed — when it should simply release the stub’s reference; reset-at-teardown was the use-after-free.
Patch Details
diff --git a/Source/JavaScriptCore/bytecode/CodeBlock.cpp b/Source/JavaScriptCore/bytecode/CodeBlock.cpp
index fd1dd5c7e09b..919f203ef225 100644
--- a/Source/JavaScriptCore/bytecode/CodeBlock.cpp
+++ b/Source/JavaScriptCore/bytecode/CodeBlock.cpp
@@ -2285,7 +2285,7 @@ void CodeBlock::jettison(Profiler::JettisonReason reason, ReoptimizationMode mod
{
ConcurrentJSLocker locker(m_lock);
forEachStructureStubInfo([&](StructureStubInfo& stubInfo) {
- stubInfo.reset(locker, this);
+ stubInfo.deref();
return IterationStatus::Continue;
});
}
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: Hossein Lotfi (@hosselot) of Trend Micro Zero Day Initiative
Key insight. The DataView byteLength lowering used the result register as a scratch register across the out-of-bounds OSR exit and mis-set the exit argument offset, so the exit could recover a clobbered/wrong length.
Patch Details
diff --git a/JSTests/stress/data-view-byte-length-oob-exit.js b/JSTests/stress/data-view-byte-length-oob-exit.js
new file mode 100644
index 000000000000..5c350321b492
--- /dev/null
+++ b/JSTests/stress/data-view-byte-length-oob-exit.js
@@ -0,0 +1,18 @@
+const crash_point= 0x41414141;
+const loop_start = crash_point&0xffffff00;
+const loop_end = crash_point+1;
+for (let i=0; i<5000; i++){
+ const get_byte_length = arr => arr.byteLength;
+ const ab = new ArrayBuffer(99, {maxByteLength: 99});
+ const dv = new DataView(ab, 1, 44);
+ for (let counter = loop_start; counter < loop_end; counter++) {
+ if (counter == loop_end-2) {
+ ab.resize(1);
+ get_byte_length(1);
+ }else{
+ try {
+ get_byte_length(dv);
+ } catch (e) {}
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp b/Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp
index 7e662ba3b9a0..7b533ce85a9b 100644
--- a/Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp
+++ b/Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp
@@ -5948,12 +5948,12 @@ IGNORE_CLANG_WARNINGS_END
#else
PatchpointValue* patchpoint = m_out.patchpoint(Int32);
#endif
- patchpoint->appendSomeRegister(base);
- patchpoint->clobber(RegisterSetBuilder::macroClobberedGPRs());
- patchpoint->numGPScratchRegisters = 2;
-
if (typedArrayType.has_value() && typedArrayType.value() == TypeDataView) {
- unsigned osrExitArgumentOffset = patchpoint->numChildren();
+ patchpoint->appendSomeRegister(base);
+ patchpoint->clobber(RegisterSetBuilder::macroClobberedGPRs());
+ patchpoint->numGPScratchRegisters = 3;
+
+ unsigned osrExitArgumentOffset = patchpoint->numChildren() + /* result */ 1;
OSRExitDescriptor* exitDescriptor = appendOSRExitDescriptor(jsValueValue(base), m_node);
patchpoint->appendColdAnys(buildExitArguments(exitDescriptor, m_origin.forExit, jsValueValue(base)));
@@ -5969,19 +5969,24 @@ IGNORE_CLANG_WARNINGS_END
GPRReg baseGPR = params[1].gpr();
GPRReg scratch1GPR = params.gpScratch(0);
GPRReg scratch2GPR = params.gpScratch(1);
+ GPRReg scratch3GPR = params.gpScratch(2);
RefPtr<OSRExitHandle> handle = exitDescriptor->emitOSRExitLater(*state, OutOfBounds, origin, params, nodeIndex, osrExitArgumentOffset);
RefPtr<FTL::JITCode> jitCode = state->jitCode;
- auto [outOfBounds, doneCases] = jit.loadDataViewByteLength(baseGPR, resultGPR, scratch1GPR, scratch2GPR, TypeDataView);
+ auto [outOfBounds, doneCases] = jit.loadDataViewByteLength(baseGPR, scratch1GPR, scratch2GPR, scratch3GPR, TypeDataView);
jit.addLinkTask([=, protectedJitCode = jitCode, outOfBoundsJump = outOfBounds](LinkBuffer& linkBuffer) {
linkBuffer.link(outOfBoundsJump, linkBuffer.locationOf<NoPtrTag>(handle->label));
});
doneCases.link(&jit);
+ jit.move(scratch1GPR, resultGPR);
});
return patchpoint;
}
+ patchpoint->appendSomeRegister(base);
+ patchpoint->clobber(RegisterSetBuilder::macroClobberedGPRs());
+ patchpoint->numGPScratchRegisters = 2;
patchpoint->setGenerator([=] (CCallHelpers& jit, const StackmapGenerationParams& params) {
JIT_COMMENT(jit, "typedArrayLength");
AllowMacroScratchRegisterUsage allowScratch(jit);
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: An app may be able to access sensitive user data
Researchers: Wojciech Regula of SecuRing (wojciechregula.blog)
Key insight. Media-capture permission grants were cached and reused by origin without regard to whether the page came from a web archive, so a crafted archive could inherit a prior grant; forcing archive-loaded pages to always prompt (and never cache) closes the bypass.
Patch Details
diff --git a/Source/WebKit/UIProcess/UserMediaPermissionRequestManagerProxy.cpp b/Source/WebKit/UIProcess/UserMediaPermissionRequestManagerProxy.cpp
index 286af00b7156..27279c6e0c33 100644
--- a/Source/WebKit/UIProcess/UserMediaPermissionRequestManagerProxy.cpp
+++ b/Source/WebKit/UIProcess/UserMediaPermissionRequestManagerProxy.cpp
@@ -267,7 +267,12 @@ void UserMediaPermissionRequestManagerProxy::denyRequest(UserMediaPermissionRequ
ALWAYS_LOG(LOGIDENTIFIER, request.userMediaID() ? request.userMediaID()->toUInt64() : 0, ", reason: ", reason);
- if (reason == UserMediaPermissionRequestProxy::UserMediaAccessDenialReason::PermissionDenied)
+ bool shouldCacheResult = true;
+#if ENABLE(WEB_ARCHIVE)
+ shouldCacheResult = !page->didLoadWebArchive();
+#endif
+
+ if (reason == UserMediaPermissionRequestProxy::UserMediaAccessDenialReason::PermissionDenied && shouldCacheResult)
m_deniedRequests.append(DeniedRequest { request.mainFrameID(), request.userMediaDocumentSecurityOrigin(), request.topLevelDocumentSecurityOrigin(), request.requiresAudioCapture(), request.requiresVideoCapture(), request.requiresDisplayCapture() });
if (auto callback = request.decisionCompletionHandler()) {
@@ -299,7 +304,12 @@ void UserMediaPermissionRequestManagerProxy::grantRequest(UserMediaPermissionReq
page->willStartCapture(request, [callback = WTFMove(callback)]() mutable {
callback(true);
});
- m_grantedRequests.append(request);
+ bool shouldCacheResult = true;
+#if ENABLE(WEB_ARCHIVE)
+ shouldCacheResult = !page->didLoadWebArchive();
+#endif
+ if (shouldCacheResult)
+ m_grantedRequests.append(request);
if (request.requiresAudioCapture())
m_grantedAudioFrames.add(request.frameID());
if (request.requiresVideoCapture())
@@ -442,6 +452,11 @@ bool UserMediaPermissionRequestManagerProxy::hasGrantedRequest(std::optional<Fra
if (!isUserGesturePriviledged && inactiveMediaCaptureStreamDuration().minutes() > page->protectedPreferences()->inactiveMediaCaptureStreamRepromptWithoutUserGestureIntervalInMinutes())
return false;
+#if ENABLE(WEB_ARCHIVE)
+ if (page->didLoadWebArchive())
+ return false;
+#endif
+
bool checkForAudio = needsAudio;
bool checkForVideo = needsVideo;
for (Ref grantedRequest : m_grantedRequests) {
diff --git a/Source/WebKit/UIProcess/WebPageProxy.cpp b/Source/WebKit/UIProcess/WebPageProxy.cpp
index f07d5b338236..7ec2f1fa8245 100644
--- a/Source/WebKit/UIProcess/WebPageProxy.cpp
+++ b/Source/WebKit/UIProcess/WebPageProxy.cpp
@@ -12814,6 +12814,48 @@ void WebPageProxy::revokeGeolocationAuthorizationToken(const String& authorizati
internals().protectedGeolocationPermissionRequestManager()->revokeAuthorizationToken(authorizationToken);
}
+#if ENABLE(WEB_ARCHIVE)
+bool WebPageProxy::shouldAlwaysPromptForPermission(PermissionName permissionName) const
+{
+ if (!didLoadWebArchive())
+ return false;
+
+ switch (permissionName) {
+ // All cached permissions that are supported by platforms that support
+ // WEB_ARCHIVE should always prompt.
+ case PermissionName::Camera:
+ case PermissionName::Geolocation:
+ case PermissionName::Microphone:
+ break;
+
+ // Notifications are not available in ephemeral sessions.
+ case PermissionName::Notifications:
+ case PermissionName::Push:
+
+ // Orientation and motion data are not emitted.
+ case PermissionName::Accelerometer:
+ case PermissionName::Gyroscope:
+ case PermissionName::Magnetometer:
+
+ // These are not persistent permissions.
+ case PermissionName::BackgroundFetch:
+ case PermissionName::DisplayCapture:
+ case PermissionName::ScreenWakeLock:
+ case PermissionName::SpeakerSelection:
+ case PermissionName::StorageAccess:
+
+ // Not supported.
+ case PermissionName::Bluetooth:
+ case PermissionName::Midi:
+ case PermissionName::Nfc:
+ ASSERT_NOT_REACHED();
+ break;
+ }
+
+ return true;
+}
+#endif
+
void WebPageProxy::queryPermission(const ClientOrigin& clientOrigin, const PermissionDescriptor& descriptor, CompletionHandler<void(std::optional<PermissionState>)>&& completionHandler)
{
bool canAPISucceed = true;
@@ -12822,8 +12864,8 @@ void WebPageProxy::queryPermission(const ClientOrigin& clientOrigin, const Permi
String name;
#if ENABLE(WEB_ARCHIVE)
- if (didLoadWebArchive()) {
- completionHandler(PermissionState::Denied);
+ if (shouldAlwaysPromptForPermission(descriptor.name)) {
+ completionHandler(PermissionState::Prompt);
return;
}
#endif
diff --git a/Source/WebKit/UIProcess/WebPageProxy.h b/Source/WebKit/UIProcess/WebPageProxy.h
index 490eab241948..20770b71e251 100644
--- a/Source/WebKit/UIProcess/WebPageProxy.h
+++ b/Source/WebKit/UIProcess/WebPageProxy.h
@@ -3107,6 +3107,10 @@ private:
void requestNotificationPermission(const String& originString, CompletionHandler<void(bool allowed)>&&);
+#if ENABLE(WEB_ARCHIVE)
+ bool shouldAlwaysPromptForPermission(WebCore::PermissionName) const;
+#endif
+
void didChangeContentSize(const WebCore::IntSize&);
void didChangeIntrinsicContentSize(const WebCore::IntSize&);
diff --git a/Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj b/Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj
index 51d51c95fb59..cb89c353447d 100644
--- a/Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj
+++ b/Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj
@@ -763,6 +763,7 @@
A17991881E1C994E00A505ED /* SharedBuffer.mm in Sources */ = {isa = PBXBuildFile; fileRef = A17991861E1C994E00A505ED /* SharedBuffer.mm */; };
A179918B1E1CA24100A505ED /* SharedBufferTest.cpp in Sources */ = {isa = PBXBuildFile; fileRef = A17991891E1CA24100A505ED /* SharedBufferTest.cpp */; };
A17C46592C98E4A80023F3C7 /* download.example.webarchive in Copy Resources */ = {isa = PBXBuildFile; fileRef = D20C03AE2B704A26004C414A /* download.example.webarchive */; };
+ A17C465A2C98E4A80023F3C7 /* example.webarchive in Copy Resources */ = {isa = PBXBuildFile; fileRef = D20C03AE2B704A26004C414B /* example.webarchive */; };
A17C465A2C98E4BB0023F3C7 /* active-touch-events.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = F4A7CE792662D83E00228685 /* active-touch-events.html */; };
A17C465B2C98E4BB0023F3C7 /* animated-red-green-blue-repeat-infinite.gif in Copy Resources */ = {isa = PBXBuildFile; fileRef = 48354DD129F08036003E566D /* animated-red-green-blue-repeat-infinite.gif */; };
A17C465C2C98E4BB0023F3C7 /* composited.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = 0F16BED72304A1D100B4A167 /* composited.html */; };
@@ -2002,6 +2003,7 @@
A17C475F2C98E5C20023F3C7 /* enormous.svg in Copy Resources */,
A17C46AF2C98E54B0023F3C7 /* enumerateMediaDevices.html in Copy Resources */,
A17C47602C98E5C20023F3C7 /* event.ics in Copy Resources */,
+ A17C465A2C98E4A80023F3C7 /* example.webarchive in Copy Resources */,
A17C46B02C98E54B0023F3C7 /* execCopy.html in Copy Resources */,
A17C47612C98E5C20023F3C7 /* exif-orientation-8-llo.jpg in Copy Resources */,
A17C47622C98E5C20023F3C7 /* ExitFullscreenOnEnterPiP.html in Copy Resources */,
@@ -4104,6 +4106,7 @@
CEDA12402437C9EA00C28A9E /* editable-region-composited-and-non-composited-overlap.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; name = "editable-region-composited-and-non-composited-overlap.html"; path = "ios/editable-region-composited-and-non-composited-overlap.html"; sourceTree = SOURCE_ROOT; };
D04CF93E285C77C9005D6337 /* MachSendRight.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = MachSendRight.cpp; sourceTree = "<group>"; };
D20C03AE2B704A26004C414A /* download.example.webarchive */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; name = download.example.webarchive; path = Tests/WebKitCocoa/download.example.webarchive; sourceTree = "<group>"; };
+ D20C03AE2B704A26004C414B /* example.webarchive */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; name = example.webarchive; path = Tests/WebKitCocoa/example.webarchive; sourceTree = "<group>"; };
D2CC7CF22D9A8D6B00011B9E /* TestCocoaImageAndCocoaColor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; lineEnding = 0; name = TestCocoaImageAndCocoaColor.h; path = cocoa/TestCocoaImageAndCocoaColor.h; sourceTree = "<group>"; };
D2CC7CF32D9A8D6B00011B9E /* TestCocoaImageAndCocoaColor.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; name = TestCocoaImageAndCocoaColor.mm; path = cocoa/TestCocoaImageAndCocoaColor.mm; sourceTree = "<group>"; };
D2D9A6BD2D96B539004A2C92 /* icon-with-subresource.svg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; lineEnding = 0; path = "icon-with-subresource.svg"; sourceTree = "<group>"; };
@@ -4594,6 +4597,7 @@
isa = PBXGroup;
children = (
D20C03AE2B704A26004C414A /* download.example.webarchive */,
+ D20C03AE2B704A26004C414B /* example.webarchive */,
3AA7856029B6C8AA00C4374A /* TestWebKitAPIPrefix.h */,
DDF3A82928930475005920CF /* gtest.xcodeproj */,
49AEEF682407276F00C87E4C /* Info.plist */,
diff --git a/Tools/TestWebKitAPI/Tests/WebKit/GetUserMedia.mm b/Tools/TestWebKitAPI/Tests/WebKit/GetUserMedia.mm
index 8f258d6448ea..24fe09c7016e 100644
--- a/Tools/TestWebKitAPI/Tests/WebKit/GetUserMedia.mm
+++ b/Tools/TestWebKitAPI/Tests/WebKit/GetUserMedia.mm
@@ -2138,6 +2138,87 @@ TEST(WebKit, GetUserMediaWithWebThread)
}
#endif // PLATFORM(IOS_FAMILY) && !PLATFORM(MACCATALYST)
+#if ENABLE(WEB_ARCHIVE)
+TEST(GetUserMedia, OriginAndWebArchivePermission)
+{
+ TestWebKitAPI::HTTPServer server({
+ { "/"_s, { "body"_s } }
+ }, TestWebKitAPI::HTTPServer::Protocol::HttpsProxy);
+
+ auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]);
+ initializeMediaCaptureConfiguration(configuration.get());
+
+ auto storeConfiguration = adoptNS([[_WKWebsiteDataStoreConfiguration alloc] initNonPersistentConfiguration]);
+ [storeConfiguration setProxyConfiguration:@{
+ (NSString *)kCFStreamPropertyHTTPSProxyHost: @"127.0.0.1",
+ (NSString *)kCFStreamPropertyHTTPSProxyPort: @(server.port())
+ }];
+ auto dataStore = adoptNS([[WKWebsiteDataStore alloc] _initWithConfiguration:storeConfiguration.get()]);
+ [configuration setWebsiteDataStore:dataStore.get()];
+
+ auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 320, 500) configuration:configuration.get()]);
+ auto delegate = adoptNS([[UserMediaCaptureUIDelegate alloc] init]);
+
+ RetainPtr navDelegate = adoptNS([TestNavigationDelegate new]);
+
+ auto observer = adoptNS([[MediaCaptureObserver alloc] init]);
+ [webView addObserver:observer.get() forKeyPath:@"microphoneCaptureState" options:NSKeyValueObservingOptionNew context:nil];
+ [webView addObserver:observer.get() forKeyPath:@"cameraCaptureState" options:NSKeyValueObservingOptionNew context:nil];
+
+ [navDelegate allowAnyTLSCertificate];
+ [webView setUIDelegate:delegate.get()];
+ [webView setNavigationDelegate:navDelegate.get()];
+
+ [delegate setAudioDecision:WKPermissionDecisionGrant];
+
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: Hossein Lotfi (@hosselot) of Trend Micro Zero Day Initiative, Nan Wang (@eternalsakura13)
Key insight. Allocation sinking modelled array length but not per-index initialization, so it forwarded reads of never-written holes as if they were stored values; correctness requires tracking which indices are provably initialized on all reaching paths (intersecting at merges) and escaping any read of an uninitialized index.
Overview
Impact:
Description: Processing maliciously crafted web content may disclose internal states of the app
Researchers: Google Big Sleep
Background
Advisory: Apple Security Advisory
| Attribute | Value |
|---|---|
| CVE | CVE-2025-46299 |
| Bugzilla | 299518 |
| Component | WebKit |
| Bug Class | LogicError |
| Severity | medium |
| Advisory | Apple Advisory |
Fix not public. The WebKit fix for this bug is embargoed or not yet disclosed. The bug ID is confirmed from the Apple advisory, but no public commit references it.
Root Cause Analysis
A memory initialization issue was addressed with improved memory handling.
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: 이동하 (Lee Dong Ha of BoB 14th)
Key insight. The inspector animation table stored animations by raw pointer (and dereferenced a possibly-null script-execution context), so a destroyed animation left a dangling entry; switching to WeakRef and null-checking the context restores lifetime safety.
Patch Details
diff --git a/Source/WebCore/inspector/agents/InspectorAnimationAgent.cpp b/Source/WebCore/inspector/agents/InspectorAnimationAgent.cpp
index 299fa548e71e..1399bcebf310 100644
--- a/Source/WebCore/inspector/agents/InspectorAnimationAgent.cpp
+++ b/Source/WebCore/inspector/agents/InspectorAnimationAgent.cpp
@@ -362,7 +362,11 @@ Inspector::Protocol::ErrorStringOr<Ref<Inspector::Protocol::Runtime::RemoteObjec
if (!animation)
return makeUnexpected(errorString);
- auto* state = animation->protectedScriptExecutionContext()->globalObject();
+ RefPtr scriptExecutionContext = animation->scriptExecutionContext();
+ if (!scriptExecutionContext)
+ return makeUnexpected("Animation is detached from context"_s);
+
+ auto* state = scriptExecutionContext->globalObject();
auto injectedScript = m_injectedScriptManager.injectedScriptFor(state);
ASSERT(!injectedScript.hasNoValue());
@@ -588,7 +592,7 @@ void InspectorAnimationAgent::frameNavigated(LocalFrame& frame)
String InspectorAnimationAgent::findAnimationId(WebAnimation& animation)
{
for (auto& [animationId, existingAnimation] : m_animationIdMap) {
- if (existingAnimation == &animation)
+ if (existingAnimation.ptr() == &animation)
return animationId;
}
return nullString();
@@ -605,7 +609,7 @@ WebAnimation* InspectorAnimationAgent::assertAnimation(Inspector::Protocol::Erro
void InspectorAnimationAgent::bindAnimation(WebAnimation& animation, RefPtr<Inspector::Protocol::Console::StackTrace> backtrace)
{
auto animationId = makeString("animation:"_s, IdentifiersFactory::createIdentifier());
- m_animationIdMap.set(animationId, &animation);
+ m_animationIdMap.set(animationId, animation);
auto animationPayload = Inspector::Protocol::Animation::Animation::create()
.setAnimationId(animationId)
diff --git a/Source/WebCore/inspector/agents/InspectorAnimationAgent.h b/Source/WebCore/inspector/agents/InspectorAnimationAgent.h
index 1c5b538fe4d3..5eb7009f0189 100644
--- a/Source/WebCore/inspector/agents/InspectorAnimationAgent.h
+++ b/Source/WebCore/inspector/agents/InspectorAnimationAgent.h
@@ -100,7 +100,7 @@ private:
Inspector::InjectedScriptManager& m_injectedScriptManager;
WeakRef<Page> m_inspectedPage;
- MemoryCompactRobinHoodHashMap<Inspector::Protocol::Animation::AnimationId, WebAnimation*> m_animationIdMap;
+ HashMap<Inspector::Protocol::Animation::AnimationId, WeakRef<WebAnimation, WeakPtrImplWithEventTargetData>> m_animationIdMap;
WeakHashMap<WebAnimation, Ref<Inspector::Protocol::Console::StackTrace>, WeakPtrImplWithEventTargetData> m_animationsPendingBinding;
Timer m_animationBindingTimer;
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: A website may exfiltrate image data cross-origin
Researchers: Tom Van Goethem
Key insight. Committing an OffscreenCanvas bitmap to its placeholder canvas failed to carry the origin-clean/tainted flag, so a canvas holding cross-origin pixels was treated as readable; propagating and applying the taint blocks the cross-origin read-back.
Patch Details
diff --git a/LayoutTests/http/tests/security/offscreen-canvas-remote-read-remote-image-expected.txt b/LayoutTests/http/tests/security/offscreen-canvas-remote-read-remote-image-expected.txt
new file mode 100644
index 000000000000..c4752ac3ae39
--- /dev/null
+++ b/LayoutTests/http/tests/security/offscreen-canvas-remote-read-remote-image-expected.txt
@@ -0,0 +1,11 @@
+Test that offscreen canvas does not allow leaking cross-site image.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+PASS Calling toDataURL() on an untainted canvas was allowed.
+PASS Calling toDataURL() on remote image was not allowed - Threw error: (SecurityError: The operation is insecure.).
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/http/tests/security/offscreen-canvas-remote-read-remote-image.html b/LayoutTests/http/tests/security/offscreen-canvas-remote-read-remote-image.html
new file mode 100644
index 000000000000..a272ee97eb19
--- /dev/null
+++ b/LayoutTests/http/tests/security/offscreen-canvas-remote-read-remote-image.html
@@ -0,0 +1,47 @@
+<script src="/js-test-resources/js-test.js"></script>
+<body>
+ <canvas width="200" height="200"></canvas>
+ <script>
+ function testToDataURL(canvas, state, shouldSucceed)
+ {
+ state = "Calling toDataURL() on " + state;
+ try {
+ var dataURL = canvas.toDataURL();
+ debug((shouldSucceed ? "PASS " : "FAIL ") + state + " was allowed.");
+ } catch (e) {
+ debug((shouldSucceed ? "FAIL " : "PASS ") + state + " was not allowed - Threw error: (" + e + ").");
+ }
+ }
+
+ (async () => {
+ description("Test that offscreen canvas does not allow leaking cross-site image.");
+
+ window.jsTestIsAsync = true;
+
+ const image = new Image;
+ image.src = "http://localhost:8000/security/resources/abe.png";
+ await image.decode();
+
+ const canvas = document.querySelector('canvas');
+
+ testToDataURL(canvas, "an untainted canvas", true);
+
+ const offscreenCanvas = canvas.transferControlToOffscreen();
+ const offscreenContext = offscreenCanvas.getContext('bitmaprenderer');
+ const imageBitmapLeak = await createImageBitmap(image);
+
+ offscreenContext.transferFromImageBitmap(imageBitmapLeak);
+
+ await new Promise((resolve) => {
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ resolve();
+ });
+ });
+ });
+
+ testToDataURL(canvas, "remote image", false);
+ finishJSTest();
+ })();
+ </script>
+</body>
diff --git a/Source/WebCore/html/OffscreenCanvas.cpp b/Source/WebCore/html/OffscreenCanvas.cpp
index ddc5b6060732..7788ce0845fa 100644
--- a/Source/WebCore/html/OffscreenCanvas.cpp
+++ b/Source/WebCore/html/OffscreenCanvas.cpp
@@ -421,8 +421,8 @@ void OffscreenCanvas::commitToPlaceholderCanvas()
RefPtr imageBuffer = m_context->surfaceBufferToImageBuffer(CanvasRenderingContext::SurfaceBuffer::DisplayBuffer);
if (!imageBuffer)
return;
- m_placeholderSource->setPlaceholderBuffer(*imageBuffer, m_context->isOpaque());
- }
+ m_placeholderSource->setPlaceholderBuffer(*imageBuffer, m_context->canvasBase().originClean(), m_context->isOpaque());
+}
void OffscreenCanvas::scheduleCommitToPlaceholderCanvas()
{
diff --git a/Source/WebCore/html/canvas/PlaceholderRenderingContext.cpp b/Source/WebCore/html/canvas/PlaceholderRenderingContext.cpp
index 2c1c1b40a0e9..030ad8dd8e83 100644
--- a/Source/WebCore/html/canvas/PlaceholderRenderingContext.cpp
+++ b/Source/WebCore/html/canvas/PlaceholderRenderingContext.cpp
@@ -50,7 +50,7 @@ PlaceholderRenderingContextSource::PlaceholderRenderingContextSource(Placeholder
{
}
-void PlaceholderRenderingContextSource::setPlaceholderBuffer(ImageBuffer& imageBuffer, bool opaque)
+void PlaceholderRenderingContextSource::setPlaceholderBuffer(ImageBuffer& imageBuffer, bool originClean, bool opaque)
{
auto bufferVersion = ++m_bufferVersion;
{
@@ -67,7 +67,7 @@ void PlaceholderRenderingContextSource::setPlaceholderBuffer(ImageBuffer& imageB
std::unique_ptr serializedClone = ImageBuffer::sinkIntoSerializedImageBuffer(WTFMove(clone));
if (!serializedClone)
return;
- callOnMainThread([weakPlaceholder = m_placeholder, buffer = WTFMove(serializedClone), bufferVersion, opaque] () mutable {
+ callOnMainThread([weakPlaceholder = m_placeholder, buffer = WTFMove(serializedClone), bufferVersion, originClean, opaque] () mutable {
assertIsMainThread();
RefPtr placeholder = weakPlaceholder.get();
if (!placeholder)
@@ -87,7 +87,7 @@ void PlaceholderRenderingContextSource::setPlaceholderBuffer(ImageBuffer& imageB
}
}
- placeholder->setPlaceholderBuffer(imageBuffer.releaseNonNull(), opaque);
+ placeholder->setPlaceholderBuffer(imageBuffer.releaseNonNull(), originClean, opaque);
source->m_placeholderBufferVersion = bufferVersion;
});
}
@@ -136,9 +136,15 @@ void PlaceholderRenderingContext::setContentsToLayer(GraphicsLayer& layer)
m_source->setContentsToLayer(layer, buffer.get(), m_opaque);
}
-void PlaceholderRenderingContext::setPlaceholderBuffer(Ref<ImageBuffer>&& buffer, bool opaque)
+void PlaceholderRenderingContext::setPlaceholderBuffer(Ref<ImageBuffer>&& buffer, bool originClean, bool opaque)
{
m_opaque = opaque;
+
+ // Transfer the drawn bitmap and its origin-clean flag to the output canvas.
+ if (originClean)
+ canvasBase().setOriginClean();
+ else
+ canvasBase().setOriginTainted();
canvasBase().setImageBufferAndMarkDirty(WTFMove(buffer));
}
diff --git a/Source/WebCore/html/canvas/PlaceholderRenderingContext.h b/Source/WebCore/html/canvas/PlaceholderRenderingContext.h
index 10cf7779311a..730ebd43fb05 100644
--- a/Source/WebCore/html/canvas/PlaceholderRenderingContext.h
+++ b/Source/WebCore/html/canvas/PlaceholderRenderingContext.h
@@ -45,7 +45,7 @@ public:
virtual ~PlaceholderRenderingContextSource() = default;
// Called by the offscreen context to submit the frame.
- void setPlaceholderBuffer(ImageBuffer&, bool opaque);
+ void setPlaceholderBuffer(ImageBuffer&, bool originClean, bool opaque);
// Called by the placeholder context to attach to compositor layer.
void setContentsToLayer(GraphicsLayer&, ImageBuffer*, bool opaque);
@@ -69,7 +69,7 @@ public:
HTMLCanvasElement& canvas() const;
Ref<HTMLCanvasElement> protectedCanvas() const { return canvas(); }
IntSize size() const;
- void setPlaceholderBuffer(Ref<ImageBuffer>&&, bool opaque);
+ void setPlaceholderBuffer(Ref<ImageBuffer>&&, bool originClean, bool opaque);
PlaceholderRenderingContextSource& source() const { return m_source; }
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: Nan Wang (@eternalsakura13)
Key insight. This is a mitigation-by-disable: WebKit turned off the entire array-allocation-sinking optimization by default rather than fixing a specific check, which strongly implies an escape-analysis/materialization soundness bug in the JIT phase whose exact faulty predicate is not visible in this one-line diff.
Patch Details
diff --git a/Source/JavaScriptCore/runtime/OptionsList.h b/Source/JavaScriptCore/runtime/OptionsList.h
index 3d65cb28ba8e..5b0750dd2521 100644
--- a/Source/JavaScriptCore/runtime/OptionsList.h
+++ b/Source/JavaScriptCore/runtime/OptionsList.h
@@ -629,7 +629,7 @@ bool hasCapacityToUseLargeGigacage();
v(Bool, forceAllFunctionsToUseSIMD, false, Normal, "Force all functions to act conservatively w.r.t fp/vector registers for testing."_s) \
v(Bool, useOMGInlining, true, Normal, "Use OMG inlining"_s) \
v(Bool, freeRetiredWasmCode, true, Normal, "free BBQ/OMG-OSR wasm code once it's no longer reachable."_s) \
- v(Bool, useArrayAllocationSinking, true, Normal, nullptr) \
+ v(Bool, useArrayAllocationSinking, false, Normal, nullptr) \
v(Bool, dumpFTLCodeSize, false, Normal, nullptr) \
v(Bool, dumpOptimizationTracing, false, Normal, nullptr) \
v(Unsigned, markedBlockDumpInfoCount, 0, Normal, nullptr) /* FIXME: rdar://139998916 */ \
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: an anonymous researcher
Key insight. The bug is a shallow-copy aliasing defect in DFG block cloning: node kinds whose OpInfo points to mutable per-node side-table data were cloned as ‘Common’ (bitwise), so clone and original shared one data object – the fix makes them ‘Special’ and allocates a fresh Bag entry per clone.
Patch Details
diff --git a/JSTests/stress/loop-unrolling-multi-get-and-put-by-offset.js b/JSTests/stress/loop-unrolling-multi-get-and-put-by-offset.js
new file mode 100644
index 000000000000..fe7c028c6b82
--- /dev/null
+++ b/JSTests/stress/loop-unrolling-multi-get-and-put-by-offset.js
@@ -0,0 +1,18 @@
+//@ runDefault("--forceEagerCompilation=true")
+function foo() {
+ const v0 = [];
+ let v1 = 0;
+
+ do {
+ for (let v5 = v1; v5 < 7; v5++) {
+ v0.c **= v5;
+ }
+ v1++;
+ } while (v1 < 9);
+
+ return v0;
+}
+
+for(let i = 0; i < 32; i++) {
+ foo();
+}
diff --git a/Source/JavaScriptCore/dfg/DFGCloneHelper.cpp b/Source/JavaScriptCore/dfg/DFGCloneHelper.cpp
index e49246b8ec63..945752db98c0 100644
--- a/Source/JavaScriptCore/dfg/DFGCloneHelper.cpp
+++ b/Source/JavaScriptCore/dfg/DFGCloneHelper.cpp
@@ -119,24 +119,27 @@ Node* CloneHelper::cloneNodeImpl(BasicBlock* into, Node* node)
return edge ? Edge(cloneNode(into, edge.node()), edge.useKind()) : Edge();
};
- switch (nodeCloneStatusFor(node->op())) {
- case NodeCloneStatus::Common: {
+ auto cloneEdges = [&](Node* node, Node* clone) {
if (node->hasVarArgs()) {
size_t firstChild = m_graph.m_varArgChildren.size();
m_graph.doToAllChildren(node, [&](Edge& edge) {
m_graph.m_varArgChildren.append(cloneEdge(edge));
});
- Node* clone = into->cloneAndAppend(m_graph, node);
clone->children.setFirstChild(firstChild);
return clone;
}
- Node* clone = into->cloneAndAppend(m_graph, node);
clone->child1() = cloneEdge(node->child1());
clone->child2() = cloneEdge(node->child2());
clone->child3() = cloneEdge(node->child3());
return clone;
+ };
+
+ switch (nodeCloneStatusFor(node->op())) {
+ case NodeCloneStatus::Common: {
+ Node* clone = into->cloneAndAppend(m_graph, node);
+ return cloneEdges(node, clone);
}
case NodeCloneStatus::Special:
@@ -144,16 +147,46 @@ Node* CloneHelper::cloneNodeImpl(BasicBlock* into, Node* node)
case Branch: {
Node* clone = into->cloneAndAppend(m_graph, node);
clone->setOpInfo(OpInfo(m_graph.m_branchData.add(WTFMove(*node->branchData()))));
- clone->child1() = cloneEdge(node->child1());
- return clone;
+ return cloneEdges(node, clone);
}
case Switch: {
Node* clone = into->cloneAndAppend(m_graph, node);
SwitchData& cloneData = *m_graph.m_switchData.add();
cloneData = *clone->switchData();
clone->setOpInfo(OpInfo(&cloneData));
- clone->child1() = cloneEdge(node->child1());
- return clone;
+ return cloneEdges(node, clone);
+ }
+ case MultiGetByOffset: {
+ Node* clone = into->cloneAndAppend(m_graph, node);
+ MultiGetByOffsetData& cloneData = *m_graph.m_multiGetByOffsetData.add();
+ cloneData = node->multiGetByOffsetData();
+ clone->setOpInfo(OpInfo(&cloneData));
+ return cloneEdges(node, clone);
+ }
+ case MultiPutByOffset: {
+ Node* clone = into->cloneAndAppend(m_graph, node);
+ MultiPutByOffsetData& cloneData = *m_graph.m_multiPutByOffsetData.add();
+ cloneData = node->multiPutByOffsetData();
+ clone->setOpInfo(OpInfo(&cloneData));
+ return cloneEdges(node, clone);
+ }
+ case CallVarargs:
+ case ConstructVarargs:
+ case TailCallVarargsInlinedCaller:
+ case TailCallForwardVarargsInlinedCaller: {
+ Node* clone = into->cloneAndAppend(m_graph, node);
+ CallVarargsData& cloneData = *m_graph.m_callVarargsData.add();
+ cloneData = *node->callVarargsData();
+ clone->setOpInfo(OpInfo(&cloneData));
+ return cloneEdges(node, clone);
+ }
+ case LoadVarargs:
+ case VarargsLength: {
+ Node* clone = into->cloneAndAppend(m_graph, node);
+ LoadVarargsData& cloneData = *m_graph.m_loadVarargsData.add();
+ cloneData = *node->loadVarargsData();
+ clone->setOpInfo(OpInfo(&cloneData));
+ return cloneEdges(node, clone);
}
default:
RELEASE_ASSERT_NOT_REACHED();
diff --git a/Source/JavaScriptCore/dfg/DFGCloneHelper.h b/Source/JavaScriptCore/dfg/DFGCloneHelper.h
index 2a55975a54f9..24160016493f 100644
--- a/Source/JavaScriptCore/dfg/DFGCloneHelper.h
+++ b/Source/JavaScriptCore/dfg/DFGCloneHelper.h
@@ -173,7 +173,7 @@ BasicBlock* CloneHelper::cloneBlock(BasicBlock* const block, const CustomizeSucc
CLONE_STATUS(Call, Common) \
CLONE_STATUS(CallCustomAccessorGetter, Common) \
CLONE_STATUS(CallDirectEval, Common) \
- CLONE_STATUS(CallVarargs, Common) \
+ CLONE_STATUS(CallVarargs, Special) \
CLONE_STATUS(Check, Common) \
CLONE_STATUS(CheckArray, Common) \
CLONE_STATUS(CheckBadValue, Common) \
@@ -194,7 +194,7 @@ BasicBlock* CloneHelper::cloneBlock(BasicBlock* const block, const CustomizeSucc
CLONE_STATUS(CompareStrictEq, Common) \
CLONE_STATUS(ConstantStoragePointer, Common) \
CLONE_STATUS(Construct, Common) \
- CLONE_STATUS(ConstructVarargs, Common) \
+ CLONE_STATUS(ConstructVarargs, Special) \
CLONE_STATUS(CreateActivation, Common) \
CLONE_STATUS(CreateDirectArguments, Common) \
CLONE_STATUS(CreateRest, Common) \
@@ -271,7 +271,7 @@ BasicBlock* CloneHelper::cloneBlock(BasicBlock* const block, const CustomizeSucc
CLONE_STATUS(JSConstant, Common) \
CLONE_STATUS(Jump, Common) \
CLONE_STATUS(LoadMapValue, Common) \
- CLONE_STATUS(LoadVarargs, Common) \
+ CLONE_STATUS(LoadVarargs, Special) \
CLONE_STATUS(LogicalNot, Common) \
CLONE_STATUS(LoopHint, Common) \
CLONE_STATUS(MakeAtomString, Common) \
@@ -284,8 +284,8 @@ BasicBlock* CloneHelper::cloneBlock(BasicBlock* const block, const CustomizeSucc
CLONE_STATUS(MapSet, Common) \
CLONE_STATUS(MatchStructure, Common) \
CLONE_STATUS(MovHint, Common) \
- CLONE_STATUS(MultiGetByOffset, Common) \
- CLONE_STATUS(MultiPutByOffset, Common) \
+ CLONE_STATUS(MultiGetByOffset, Special) \
+ CLONE_STATUS(MultiPutByOffset, Special) \
CLONE_STATUS(NewArray, Common) \
CLONE_STATUS(NewArrayBuffer, Common) \
CLONE_STATUS(NewArrayWithButterfly, Common) \
@@ -346,9 +346,9 @@ BasicBlock* CloneHelper::cloneBlock(BasicBlock* const block, const CustomizeSucc
CLONE_STATUS(StringSubstring, Common) \
CLONE_STATUS(StrCat, Common) \
CLONE_STATUS(Switch, Special) \
- CLONE_STATUS(TailCallForwardVarargsInlinedCaller, Common) \
+ CLONE_STATUS(TailCallForwardVarargsInlinedCaller, Special) \
CLONE_STATUS(TailCallInlinedCaller, Common) \
- CLONE_STATUS(TailCallVarargsInlinedCaller, Common) \
+ CLONE_STATUS(TailCallVarargsInlinedCaller, Special) \
CLONE_STATUS(ToLength, Common) \
CLONE_STATUS(ToLowerCase, Common) \
CLONE_STATUS(ToPrimitive, Common) \
@@ -370,7 +370,7 @@ BasicBlock* CloneHelper::cloneBlock(BasicBlock* const block, const CustomizeSucc
CLONE_STATUS(ValueRep, Common) \
CLONE_STATUS(ValueSub, Common) \
CLONE_STATUS(ValueToInt32, Common) \
- CLONE_STATUS(VarargsLength, Common) \
+ CLONE_STATUS(VarargsLength, Special) \
CLONE_STATUS(ZombieHint, Common)
} } // namespace JSC::DFG
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: Gary Kwong, rheza (@ginggilBesel)
Key insight. Overloading the sentinel m_min == INT32_MAX to mean ’list table’ collides with a legitimate dense switch whose case value is INT32_MAX; the fix is to carry an explicit m_isList flag (and serialize it) rather than infer table shape from a magic minimum value.
Patch Details
diff --git a/JSTests/stress/switch-imm-int32-max.js b/JSTests/stress/switch-imm-int32-max.js
new file mode 100644
index 000000000000..db7949d90eda
--- /dev/null
+++ b/JSTests/stress/switch-imm-int32-max.js
@@ -0,0 +1,16 @@
+//@ runDefault("--useConcurrentJIT=false", "--thresholdForJITAfterWarmUp=10")
+try {
+ switch (1) {
+ case 2147483647:
+ case 2147483647:
+ case 2147483647:
+ }
+} catch(e39) {
+ switch (1) {
+ case 2147483647:
+ try {} catch(e80) {}
+ }
+ switch (1) {
+ case 2147483647:
+ }
+}
diff --git a/Source/JavaScriptCore/bytecode/UnlinkedCodeBlock.h b/Source/JavaScriptCore/bytecode/UnlinkedCodeBlock.h
index 2035018f8aa3..fd8388c8aeac 100644
--- a/Source/JavaScriptCore/bytecode/UnlinkedCodeBlock.h
+++ b/Source/JavaScriptCore/bytecode/UnlinkedCodeBlock.h
@@ -115,6 +115,7 @@ struct UnlinkedSimpleJumpTable {
FixedVector<int32_t> m_branchOffsets;
int32_t m_min { 0 };
int32_t m_defaultOffset { 0 };
+ int32_t m_isList { 0 };
inline int32_t offsetForValue(int32_t value) const
{
@@ -135,7 +136,7 @@ struct UnlinkedSimpleJumpTable {
int32_t defaultOffset() const { return m_defaultOffset; }
// Returns true if this is a list-style jump table (key-offset pairs), used for sparse switches.
- bool isList() const { return m_min == INT32_MAX; }
+ bool isList() const { return !!m_isList; }
};
class UnlinkedCodeBlock : public JSCell {
diff --git a/Source/JavaScriptCore/bytecompiler/BytecodeGenerator.cpp b/Source/JavaScriptCore/bytecompiler/BytecodeGenerator.cpp
index 8b561526e282..c5eee8e4c0f9 100644
--- a/Source/JavaScriptCore/bytecompiler/BytecodeGenerator.cpp
+++ b/Source/JavaScriptCore/bytecompiler/BytecodeGenerator.cpp
@@ -4464,6 +4464,7 @@ void BytecodeGenerator::endSwitch(const Vector<Ref<Label>, 8>& labels, Expressio
auto handleSwitchList = [&](auto bytecode) {
UnlinkedSimpleJumpTable& jumpTable = m_codeBlock->unlinkedSwitchJumpTable(bytecode.m_tableIndex);
jumpTable.m_min = INT32_MAX;
+ jumpTable.m_isList = true;
Vector<int32_t> branchOffsets;
branchOffsets.reserveInitialCapacity(labels.size() * 2);
diff --git a/Source/JavaScriptCore/llint/LowLevelInterpreter32_64.asm b/Source/JavaScriptCore/llint/LowLevelInterpreter32_64.asm
index dbb989b4cf5d..e3359cf1fae6 100644
--- a/Source/JavaScriptCore/llint/LowLevelInterpreter32_64.asm
+++ b/Source/JavaScriptCore/llint/LowLevelInterpreter32_64.asm
@@ -2239,8 +2239,8 @@ llintOpWithJump(op_switch_imm, OpSwitchImm, macro (size, get, jump, dispatch)
bineq t1, Int32Tag, .opSwitchImmNotInt
+ btinz UnlinkedSimpleJumpTable::m_isList[t2], .opSwitchImmSlow
loadi UnlinkedSimpleJumpTable::m_min[t2], t3
- bieq t3, (constexpr INT32_MAX), .opSwitchImmSlow
subi t3, t0
loadp UnlinkedSimpleJumpTable::m_branchOffsets + Int32FixedVector::m_storage[t2], t3
diff --git a/Source/JavaScriptCore/llint/LowLevelInterpreter64.asm b/Source/JavaScriptCore/llint/LowLevelInterpreter64.asm
index 2c6a766dd31e..5f87e445d0e8 100644
--- a/Source/JavaScriptCore/llint/LowLevelInterpreter64.asm
+++ b/Source/JavaScriptCore/llint/LowLevelInterpreter64.asm
@@ -2407,8 +2407,8 @@ llintOpWithJump(op_switch_imm, OpSwitchImm, macro (size, get, jump, dispatch)
bqb t1, numberTag, .opSwitchImmNotInt
+ btinz UnlinkedSimpleJumpTable::m_isList[t2], .opSwitchImmSlow
loadi UnlinkedSimpleJumpTable::m_min[t2], t3
- bieq t3, (constexpr INT32_MAX), .opSwitchImmSlow
subi t3, t1
loadp UnlinkedSimpleJumpTable::m_branchOffsets + Int32FixedVector::m_storage[t2], t3
diff --git a/Source/JavaScriptCore/runtime/CachedTypes.cpp b/Source/JavaScriptCore/runtime/CachedTypes.cpp
index 7b3c4d958db4..94aa81f90bb3 100644
--- a/Source/JavaScriptCore/runtime/CachedTypes.cpp
+++ b/Source/JavaScriptCore/runtime/CachedTypes.cpp
@@ -890,6 +890,7 @@ public:
{
m_min = jumpTable.m_min;
m_defaultOffset = jumpTable.m_defaultOffset;
+ m_isList = jumpTable.m_isList;
m_branchOffsets.encode(encoder, jumpTable.m_branchOffsets);
}
@@ -897,12 +898,14 @@ public:
{
jumpTable.m_min = m_min;
jumpTable.m_defaultOffset = m_defaultOffset;
+ jumpTable.m_isList = m_isList;
m_branchOffsets.decode(decoder, jumpTable.m_branchOffsets);
}
private:
int32_t m_min;
int32_t m_defaultOffset;
+ int32_t m_isList;
CachedVector<int32_t> m_branchOffsets;
};
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: Google Big Sleep
Key insight. JSC passed unbounded string lengths into ICU whose buffer-length math is 32-bit; the fix rejects lengths that can overflow it and honors ICU’s allocation-error status instead of only asserting grow-to-fit.
Patch Details
diff --git a/Source/JavaScriptCore/runtime/StringPrototype.cpp b/Source/JavaScriptCore/runtime/StringPrototype.cpp
index 0def0bc52472..b9b4072e0409 100644
--- a/Source/JavaScriptCore/runtime/StringPrototype.cpp
+++ b/Source/JavaScriptCore/runtime/StringPrototype.cpp
@@ -1438,6 +1438,12 @@ static JSValue normalize(JSGlobalObject* globalObject, JSString* string, Normali
if (view->is8Bit() && (form == NormalizationForm::NFC || view->containsOnlyASCII()))
RELEASE_AND_RETURN(scope, string);
+ // rdar://160634825
+ // ICU isn't able to handle large strings due to buffer length calculations potentially overflowing.
+ // We'll add a length check here to catch those cases ahead of time.
+ if (view->length() >= (1 << 30))
+ return throwOutOfMemoryError(globalObject, scope);
+
const UNormalizer2* normalizer = JSC::normalizer(form);
// Since ICU does not offer functions that can perform normalization or check for
@@ -1451,6 +1457,8 @@ static JSValue normalize(JSGlobalObject* globalObject, JSString* string, Normali
RELEASE_AND_RETURN(scope, string);
int32_t normalizedStringLength = unorm2_normalize(normalizer, characters, view->length(), nullptr, 0, &status);
+ if (isICUMemoryAllocationError(status))
+ return throwOutOfMemoryError(globalObject, scope);
ASSERT(needsToGrowToProduceBuffer(status));
std::span<char16_t> buffer;
diff --git a/Source/WTF/wtf/unicode/icu/ICUHelpers.h b/Source/WTF/wtf/unicode/icu/ICUHelpers.h
index 6a415e586a33..66525e2ef44b 100644
--- a/Source/WTF/wtf/unicode/icu/ICUHelpers.h
+++ b/Source/WTF/wtf/unicode/icu/ICUHelpers.h
@@ -61,6 +61,11 @@ constexpr bool needsToGrowToProduceCString(UErrorCode errorCode)
return needsToGrowToProduceBuffer(errorCode) || errorCode == U_STRING_NOT_TERMINATED_WARNING;
}
+constexpr bool isICUMemoryAllocationError(UErrorCode errorCode)
+{
+ return errorCode == U_MEMORY_ALLOCATION_ERROR;
+}
+
namespace CallBufferProducingFunction {
template<typename CharacterType, size_t inlineCapacity, typename ...ArgumentTypes> auto& findVector(Vector<CharacterType, inlineCapacity>& buffer, ArgumentTypes&&...)
@@ -128,4 +133,5 @@ WTF_EXPORT_PRIVATE unsigned minorVersion();
using WTF::callBufferProducingFunction;
using WTF::needsToGrowToProduceCString;
using WTF::needsToGrowToProduceBuffer;
+using WTF::isICUMemoryAllocationError;
using WTF::ICUDeleter;
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: Google Big Sleep
Key insight. Every OSREntryValue must correspond to a value that actually exists at the OSR point; the try frame’s implicit exception slot held no real value, so describing it as a loadable Int64 (rather than a Void do-not-load placeholder) caused an overlapping, bogus write during loop OSR entry.
Patch Details
diff --git a/JSTests/wasm/stress/ipint-bbq-osr-check-try-implicit-slot-overlap.js b/JSTests/wasm/stress/ipint-bbq-osr-check-try-implicit-slot-overlap.js
new file mode 100644
index 000000000000..a819b55b8722
--- /dev/null
+++ b/JSTests/wasm/stress/ipint-bbq-osr-check-try-implicit-slot-overlap.js
@@ -0,0 +1,140 @@
+function instantiate(moduleBase64, importObject) {
+ let bytes = Uint8Array.fromBase64(moduleBase64);
+ return WebAssembly.instantiate(bytes, importObject);
+ }
+ const report = $.agent.report;
+ const isJIT = callerIsBBQOrOMGCompiled;
+const extra = {isJIT};
+(async function () {
+let memory0 = new WebAssembly.Memory({initial: 1850, shared: true, maximum: 2731});
+/**
+@param {I32} a0
+@param {F32} a1
+@returns {void}
+ */
+let fn0 = function (a0, a1) {
+a0?.toString(); a1?.toString();
+};
+/**
+@returns {void}
+ */
+let fn1 = function () {
+};
+/**
+@param {I32} a0
+@param {F32} a1
+@returns {[I32, F32]}
+ */
+let fn2 = function (a0, a1) {
+a0?.toString(); a1?.toString();
+return [36, 8.433544074882645e1];
+};
+/**
+@param {I32} a0
+@param {FuncRef} a1
+@returns {void}
+ */
+let fn3 = function (a0, a1) {
+a0?.toString(); a1?.toString();
+};
+let tag5 = new WebAssembly.Tag({parameters: []});
+let tag6 = new WebAssembly.Tag({parameters: ['i32', 'f32']});
+let tag9 = new WebAssembly.Tag({parameters: []});
+let global0 = new WebAssembly.Global({value: 'i32', mutable: true}, 3721432472);
+let global1 = new WebAssembly.Global({value: 'f64', mutable: true}, 548636.1280581957);
+let global3 = new WebAssembly.Global({value: 'f32', mutable: true}, 52810.0658290932);
+let global4 = new WebAssembly.Global({value: 'i64', mutable: true}, 1872919390n);
+let global7 = new WebAssembly.Global({value: 'anyfunc', mutable: true}, null);
+let global8 = new WebAssembly.Global({value: 'externref', mutable: true}, {});
+let table0 = new WebAssembly.Table({initial: 48, element: 'externref', maximum: 347});
+let table1 = new WebAssembly.Table({initial: 37, element: 'anyfunc'});
+let table2 = new WebAssembly.Table({initial: 51, element: 'externref', maximum: 276});
+let table3 = new WebAssembly.Table({initial: 77, element: 'externref'});
+let m2 = {fn3, global0, global2: global0, global7, memory0, tag5, tag6, tag9};
+let m1 = {fn0, fn1, fn2, global1, global6: global3, global8, table0, table1, table3, tag8: tag5, tag12: tag9};
+let m0 = {global3, global4, global5: global4, table2, table4: table3, tag7: tag5, tag10: tag9, tag11: tag6};
+let importObject0 = /** @type {Imports2} */ ({extra, m0, m1, m2});
+/*
+ (func (;5;) (param ) (result )
+ (local i32 i32)
+ loop ;; label = @1
+ local.get 1
+ i32.const 1
+ i32.add
+ local.tee 1
+ i32.const 43
+ i32.lt_u
+ br_if 0 (;@1;)
+ ;; BEGIN IMPORTANT
+ i32.const 1
+ i32.const 6
+ table.get 1
+ ;; BEGIN VERY IMPORTANT
+ loop (param i32 funcref) (result i32 funcref) ;; label = @3
+ try (param i32 funcref) (result i32 funcref) ;; label = @4
+ local.get 0
+ i32.const 1
+ i32.add
+ local.tee 0
+ i32.const 36
+ i32.lt_u
+ if (param i32 funcref) (result i32 funcref) ;; label = @5
+ br 2 (;@3;)
+ end
+ end
+ end
+ ;; END VERY IMPORTANT
+ table.set 1
+ ;; END IMPORTANT
+ end
+ )
+ (func (;6;) (type 18) (param i32 f32)
+ call 5
+ return)
+
+*/
+let i0 = await instantiate(
+'AGFzbQEAAAABgAETYAABf2ACf3AJcH1/cHt9f3t9YAJ/cAJ/cGACf3AAYANvcHsAYANvcHsDb3B7YANvcHsAYAN7fn8De317YAN7fn8De35/YAN7fn8AYANwfnsDfXB/YANwfnsDcH57YANwfnsAYAAAYAAAYAAAYAJ/fQJ7f2ACf30Cf31gAn99AALnAhwCbTIHbWVtb3J5MAIDug6rFQJtMQNmbjAAEgJtMQNmbjEADQJtMQNmbjIAEQJtMgNmbjMAAwVleHRyYQVpc0pJVAAAAm0yBHRhZzUEAA4CbTIEdGFnNgQAEgJtMAR0YWc3BAAPAm0xBHRhZzgEAA0CbTIEdGFnOQQADgJtMAV0YWcxMAQADwJtMAV0YWcxMQQAEgJtMQV0YWcxMgQADwJtMgdnbG9iYWwwA38BAm0xB2dsb2JhbDEDfAECbTIHZ2xvYmFsMgN/AQJtMAdnbG9iYWwzA30BAm0wB2dsb2JhbDQDfgECbTAHZ2xvYmFsNQN+AQJtMQdnbG9iYWw2A30BAm0yB2dsb2JhbDcDcAECbTEHZ2xvYmFsOANvAQJtMQZ0YWJsZTABbwEw2wICbTEGdGFibGUxAXAAJQJtMAZ0YWJsZTIBbwEzlAICbTEGdGFibGUzAW8ATQJtMAZ0YWJsZTQBbwA7AwMCDRIEFgVvAFhvARTTBm8BE6kGcABObwFNygYNFwsADwAOAAYADQAOAAwADQAJAA0ABgANBl4KfQFDC0JCjAt8AUTD6W+dAQahrQt+AULEAAt/AUHfAAt8AESAU9orGbwSsgt/AUEBC30BQ0MnTH8LfgFC7ZHWnwwLfwFBxwALewH9DIFrswgU3AOrzNvkUWHqRLwLB74BFQR0YWczBAcIZ2xvYmFsMTYDEgR0YWcwBAIDZm41AAYEdGFnMgQGBnRhYmxlOAEIBnRhYmxlNwEHB21lbW9yeTECAAhnbG9iYWwxNQMRBHRhZzEEBQNmbjQABQhnbG9iYWwxMQMKBHRhZzQECghnbG9iYWwxMAMJBnRhYmxlNgEGCGdsb2JhbDE0AxAGdGFibGU5AQkHZ2xvYmFsOQMACGdsb2JhbDEzAwwGdGFibGU1AQUIZ2xvYmFsMTIDCwnyAQkDAE4BBQEDAgUCAwAFAwEGBAQDAwIGBgEAAwADAQEFBAEBBQYFBgUBAwYEAgQABgACBQYFBAYEAAIBBQYGBQIFAgYGAQADBgAFAgEGAQUGBQUBAFQAAwUCAQIFBgYAAgAABQYEBAMDAgADAgEABgEBBAIAAQQCAAQGAQAFAQICAwUEAAYBAgAEAAYFBAYDAwUABgUGBgEAAQEAAAMFBAQBAgYGAwMGBgMCAUEUCwARAwIFAwYABQIAAwIGAwYCBQUCAUEeCwACAAYCCEEMCwABAQIIQSILAAECAghBEgsAAQMCCEELCwABBAIBQQELAAEFCjgCMAECfwNAIAFBAWoiAUErSQ0AQQFBBiUBAwIGAiAAQQFqIgBBJEkEAgwCCwsLJgELCwUAEAUPCwsZAwEHnzQg5BrOxgBBiscACwSuJgPYAQKQxA=='
+ , importObject0);
+let {fn4, fn5, global9, global10, global11, global12, global13, global14, global15, global16, memory1, table5, table6, table7, table8, table9, tag0, tag1, tag2, tag3, tag4} = /**
+ @type {{
+fn4: (a0: FuncRef, a1: I64, a2: V128) => [FuncRef, I64, V128],
+fn5: (a0: I32, a1: F32) => void,
+global9: WebAssembly.Global,
+global10: WebAssembly.Global,
+global11: WebAssembly.Global,
+global12: WebAssembly.Global,
+global13: WebAssembly.Global,
+global14: WebAssembly.Global,
+global15: WebAssembly.Global,
+global16: WebAssembly.Global,
+memory1: WebAssembly.Memory,
+table5: WebAssembly.Table,
+table6: WebAssembly.Table,
+table7: WebAssembly.Table,
+table8: WebAssembly.Table,
+table9: WebAssembly.Table,
+tag0: WebAssembly.Tag,
+tag1: WebAssembly.Tag,
+tag2: WebAssembly.Tag,
+tag3: WebAssembly.Tag,
+tag4: WebAssembly.Tag
+ }} */ (i0.instance.exports);
+report('progress');
+try {
+ for (let k=0; k<27; k++) {
+ let zzz = fn5(global9.value, global3.value);
+ if (zzz !== undefined) { throw new Error('expected undefined but return value is '+zzz); }
+ }
+} catch (e) {
+ if (e instanceof WebAssembly.Exception) {
+ } else if (e instanceof TypeError) {
+ if (e.message === 'an exported wasm function cannot contain a v128 parameter or return value') {} else { throw e; }
+ } else if (e instanceof WebAssembly.RuntimeError || e instanceof RangeError) {} else { throw e; }
+}
+})().then(() => {
+ report('after');
+}).catch(e => {
+ report('error');
+})
+
diff --git a/Source/JavaScriptCore/wasm/WasmBBQJIT.cpp b/Source/JavaScriptCore/wasm/WasmBBQJIT.cpp
index 2b40b5dc041e..a32850feeee8 100644
--- a/Source/JavaScriptCore/wasm/WasmBBQJIT.cpp
+++ b/Source/JavaScriptCore/wasm/WasmBBQJIT.cpp
@@ -3454,28 +3454,32 @@ StackMap BBQJIT::makeStackMap(const ControlData& data, Stack& enclosingStack)
StackMap stackMap(numElements);
unsigned stackMapIndex = 0;
for (unsigned i = 0; i < m_locals.size(); i ++)
- stackMap[stackMapIndex ++] = OSREntryValue(toB3Rep(m_locals[i]), toB3Type(m_localTypes[i]));
+ stackMap[stackMapIndex++] = OSREntryValue(toB3Rep(m_locals[i]), toB3Type(m_localTypes[i]));
// Do rethrow slots first because IPInt has them in a shadow stack.
for (const ControlEntry& entry : m_parser->controlStack()) {
+ if (BBQJIT::ControlData::isTry(entry.controlData)) {
+ Value exception = this->exception(entry.controlData);
+ // We don't actually have a real value here, so use Void to signify we shouldn't load it
+ stackMap[stackMapIndex++] = OSREntryValue(toB3Rep(locationOf(exception)), B3::Void);
+ }
+
unsigned numSlots = entry.controlData.implicitSlots();
- if (BBQJIT::ControlData::isTry(entry.controlData))
- ++numSlots;
- for (unsigned i = 0; i < numSlots; i ++) {
+ for (unsigned i = 0; i < numSlots; i++) {
Value exception = this->exception(entry.controlData);
- stackMap[stackMapIndex ++] = OSREntryValue(toB3Rep(locationOf(exception)), B3::Int64); // Exceptions are EncodedJSValues, so they are always Int64
+ stackMap[stackMapIndex++] = OSREntryValue(toB3Rep(locationOf(exception)), B3::Int64); // Exceptions are EncodedJSValues, so they are always Int64
}
}
for (const ControlEntry& entry : m_parser->controlStack()) {
for (const TypedExpression& expr : entry.enclosedExpressionStack)
- stackMap[stackMapIndex ++] = OSREntryValue(toB3Rep(locationOf(expr.value())), toB3Type(expr.type().kind));
+ stackMap[stackMapIndex++] = OSREntryValue(toB3Rep(locationOf(expr.value())), toB3Type(expr.type().kind));
}
for (const TypedExpression& expr : enclosingStack)
- stackMap[stackMapIndex ++] = OSREntryValue(toB3Rep(locationOf(expr.value())), toB3Type(expr.type().kind));
- for (unsigned i = 0; i < data.argumentLocations().size(); i ++)
- stackMap[stackMapIndex ++] = OSREntryValue(toB3Rep(data.argumentLocations()[i]), toB3Type(data.argumentType(i).kind));
+ stackMap[stackMapIndex++] = OSREntryValue(toB3Rep(locationOf(expr.value())), toB3Type(expr.type().kind));
+ for (unsigned i = 0; i < data.argumentLocations().size(); i++)
+ stackMap[stackMapIndex++] = OSREntryValue(toB3Rep(data.argumentLocations()[i]), toB3Type(data.argumentType(i).kind));
RELEASE_ASSERT(stackMapIndex == numElements);
unsigned bufferSize = Context::scratchBufferSlotsPerValue(m_callee.savedFPWidth()) * (BBQCallee::extraOSRValuesForLoopIndex + numElements);
diff --git a/Source/JavaScriptCore/wasm/WasmOperations.cpp b/Source/JavaScriptCore/wasm/WasmOperations.cpp
index d3c031f2611a..f7a394d83475 100644
--- a/Source/JavaScriptCore/wasm/WasmOperations.cpp
+++ b/Source/JavaScriptCore/wasm/WasmOperations.cpp
@@ -1166,6 +1166,9 @@ JSC_DEFINE_NOEXCEPT_JIT_OPERATION(operationWasmLoopOSREnterBBQJIT, void, (Probe:
const StackMap& stackMap = entryData.values();
auto writeValueToRep = [&](uint64_t* bufferSlot, const OSREntryValue& value) {
B3::Type type = value.type();
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to memory corruption
Researchers: Google Big Sleep
Key insight. If any object is referenced by raw pointers (FieldTypes into a Subtype), that object must be strongly retained; retaining only the expanded type left the unexpanded Subtype collectable.
Patch Details
diff --git a/JSTests/wasm/regress/298194.js b/JSTests/wasm/regress/298194.js
new file mode 100644
index 000000000000..7707bb7ce25f
--- /dev/null
+++ b/JSTests/wasm/regress/298194.js
@@ -0,0 +1,46 @@
+load("../spec-harness/wasm-module-builder-gc.js");
+
+function createWasmModule() {
+ let builder = new WasmModuleBuilder();
+
+ // Use a Subtype for B by setting isFinal = false.
+ const isFinal = false;
+ const supertypeIndex = kNoSuperType;
+
+ const typeBIdx = builder.addStruct([], supertypeIndex, isFinal);
+ const typeAIdx = builder.addStruct([makeField(wasmRefType(typeBIdx), true)]);
+
+ const refA = wasmRefType(typeAIdx);
+ const refB = wasmRefType(typeBIdx);
+
+ const sigNewA = builder.addType(makeSig([refB], [refA]));
+ builder.addFunction("new_A", sigNewA)
+ .addBody([
+ kExprLocalGet, 0,
+ kGCPrefix, kExprStructNew, ...wasmUnsignedLeb(typeAIdx)
+ ]).exportFunc();
+ const sigNewB = builder.addType(makeSig([], [refB]));
+ builder.addFunction("new_B", sigNewB)
+ .addBody([ kGCPrefix, kExprStructNewDefault, ...wasmUnsignedLeb(typeBIdx) ]).exportFunc();
+
+ return builder.instantiate();
+}
+
+let obj = null;
+{
+ let inst = createWasmModule();
+ let exports = inst.exports;
+ let objB = exports.new_B();
+ obj = exports.new_A(objB);
+}
+
+gc();
+
+let dummy = (new WasmModuleBuilder()).instantiate();
+dummy = null;
+
+// Trigger TypeInformation::tryCleanup() to free the TypeDefinition.
+gc();
+
+// Trigger assertion failure during GC.
+gc();
\ No newline at end of file
diff --git a/JSTests/wasm/spec-harness/wasm-module-builder-gc.js b/JSTests/wasm/spec-harness/wasm-module-builder-gc.js
new file mode 100644
index 000000000000..5c96cbba7980
--- /dev/null
+++ b/JSTests/wasm/spec-harness/wasm-module-builder-gc.js
@@ -0,0 +1,2492 @@
+// Copyright 2016 the V8 project authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// This is a 'wasm-module-builder.js' circa 2025 with support for the GC proposal,
+// in particular Subtype-wrapped types.
+
+// Used for encoding f32 and double constants to bits.
+let byte_view = new Uint8Array(8);
+let data_view = new DataView(byte_view.buffer);
+
+// The bytes function receives one of
+// - several arguments, each of which is either a number or a string of length
+// 1; if it's a string, the charcode of the contained character is used.
+// - a single array argument containing the actual arguments
+// - a single string; the returned buffer will contain the char codes of all
+// contained characters.
+function bytes(...input) {
+ if (input.length == 1 && typeof input[0] == 'array') input = input[0];
+ if (input.length == 1 && typeof input[0] == 'string') {
+ let len = input[0].length;
+ let view = new Uint8Array(len);
+ for (let i = 0; i < len; i++) view[i] = input[0].charCodeAt(i);
+ return view.buffer;
+ }
+ let view = new Uint8Array(input.length);
+ for (let i = 0; i < input.length; i++) {
+ let val = input[i];
+ if (typeof val == 'string') {
+ if (val.length != 1) {
+ throw new Error('string inputs must have length 1');
+ }
+ val = val.charCodeAt(0);
+ }
+ view[i] = val | 0;
+ }
+ return view.buffer;
+}
+
+// Header declaration constants
+var kWasmH0 = 0;
+var kWasmH1 = 0x61;
+var kWasmH2 = 0x73;
+var kWasmH3 = 0x6d;
+
+var kWasmV0 = 0x1;
+var kWasmV1 = 0;
+var kWasmV2 = 0;
+var kWasmV3 = 0;
+
+var kHeaderSize = 8;
+var kPageSize = 65536;
+var kSpecMaxPages = 65536;
+var kMaxVarInt32Size = 5;
+var kMaxVarInt64Size = 10;
+var kSpecMaxFunctionParams = 1_000;
+
+let kDeclNoLocals = 0;
+
+// Section declaration constants
+let kUnknownSectionCode = 0;
+let kTypeSectionCode = 1; // Function signature declarations
+let kImportSectionCode = 2; // Import declarations
+let kFunctionSectionCode = 3; // Function declarations
+let kTableSectionCode = 4; // Indirect function table and other tables
+let kMemorySectionCode = 5; // Memory attributes
+let kGlobalSectionCode = 6; // Global declarations
+let kExportSectionCode = 7; // Exports
+let kStartSectionCode = 8; // Start function declaration
+let kElementSectionCode = 9; // Elements section
+let kCodeSectionCode = 10; // Function code
+let kDataSectionCode = 11; // Data segments
+let kDataCountSectionCode = 12; // Data segment count (between Element & Code)
+let kTagSectionCode = 13; // Tag section (between Memory & Global)
+let kStringRefSectionCode = 14; // Stringref literals section (between Tag & Global)
+let kLastKnownSectionCode = 14;
+
+// Name section types
+let kModuleNameCode = 0;
+let kFunctionNamesCode = 1;
+let kLocalNamesCode = 2;
+
+let kWasmSharedTypeForm = 0x65;
+let kWasmFunctionTypeForm = 0x60;
+let kWasmStructTypeForm = 0x5f;
+let kWasmArrayTypeForm = 0x5e;
+let kWasmSubtypeForm = 0x50;
+let kWasmSubtypeFinalForm = 0x4f;
+let kWasmRecursiveTypeGroupForm = 0x4e;
+let kWasmDescriptorTypeForm = 0x4d;
+let kWasmDescribesTypeForm = 0x4c;
+
+let kNoSuperType = 0xFFFFFFFF;
+
+let kLimitsNoMaximum = 0x00;
+let kLimitsWithMaximum = 0x01;
+let kLimitsSharedNoMaximum = 0x02;
+let kLimitsSharedWithMaximum = 0x03;
+let kLimitsMemory64NoMaximum = 0x04;
+let kLimitsMemory64WithMaximum = 0x05;
+let kLimitsMemory64SharedNoMaximum = 0x06;
+let kLimitsMemory64SharedWithMaximum = 0x07;
+
+// Segment flags
+let kActiveNoIndex = 0;
+let kPassive = 1;
+let kActiveWithIndex = 2;
+let kDeclarative = 3;
+let kPassiveWithElements = 5;
+let kDeclarativeWithElements = 7;
+
+// Function declaration flags
+let kDeclFunctionName = 0x01;
+let kDeclFunctionImport = 0x02;
+let kDeclFunctionLocals = 0x04;
+let kDeclFunctionExport = 0x08;
+
+// Value types and related
+let kWasmVoid = 0x40;
+let kWasmI32 = 0x7f;
+let kWasmI64 = 0x7e;
+let kWasmF32 = 0x7d;
+let kWasmF64 = 0x7c;
+let kWasmS128 = 0x7b;
+let kWasmI8 = 0x78;
+let kWasmI16 = 0x77;
+let kWasmF16 = 0x76;
+
+// These are defined as negative integers to distinguish them from positive type
+// indices.
+let kWasmNullFuncRef = -0x0d;
+let kWasmNullExternRef = -0x0e;
+let kWasmNullRef = -0x0f;
+let kWasmFuncRef = -0x10;
+let kWasmAnyFunc = kWasmFuncRef; // Alias named as in the JS API spec
+let kWasmExternRef = -0x11;
+let kWasmAnyRef = -0x12;
+let kWasmEqRef = -0x13;
+let kWasmI31Ref = -0x14;
+let kWasmStructRef = -0x15;
+let kWasmArrayRef = -0x16;
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: Hossein Lotfi (@hosselot) of Trend Micro Zero Day Initiative
Key insight. The wasm FunctionParser aliased its signature TypeDefinition by bare reference instead of retaining it, so the object could be freed mid-parse; holding a Ref<const TypeDefinition> restores lifetime safety.
Patch Details
diff --git a/Source/JavaScriptCore/wasm/WasmFunctionParser.h b/Source/JavaScriptCore/wasm/WasmFunctionParser.h
index 574b54ef4bfe..2d228035afb0 100644
--- a/Source/JavaScriptCore/wasm/WasmFunctionParser.h
+++ b/Source/JavaScriptCore/wasm/WasmFunctionParser.h
@@ -377,7 +377,7 @@ private:
Stack m_expressionStack;
ControlStack m_controlStack;
Vector<Type, 16> m_locals;
- const TypeDefinition& m_signature;
+ Ref<const TypeDefinition> m_signature;
const ModuleInformation& m_info;
Vector<uint32_t> m_localInitStack;
@@ -432,8 +432,8 @@ auto FunctionParser<Context>::parse() -> Result
{
uint32_t localGroupsCount;
- WASM_PARSER_FAIL_IF(!m_signature.is<FunctionSignature>(), "type signature was not a function signature"_s);
- const auto& signature = *m_signature.as<FunctionSignature>();
+ WASM_PARSER_FAIL_IF(!m_signature->template is<FunctionSignature>(), "type signature was not a function signature"_s);
+ const auto& signature = *m_signature->template as<FunctionSignature>();
if (signature.numVectors() || signature.numReturnVectors())
m_context.notifyFunctionUsesSIMD();
@@ -485,8 +485,8 @@ auto FunctionParser<Context>::parse() -> Result
template<typename Context>
auto FunctionParser<Context>::parseConstantExpression() -> Result
{
- WASM_PARSER_FAIL_IF(!m_signature.is<FunctionSignature>(), "type signature was not a function signature"_s);
- const auto& signature = *m_signature.as<FunctionSignature>();
+ WASM_PARSER_FAIL_IF(!m_signature->template is<FunctionSignature>(), "type signature was not a function signature"_s);
+ const auto& signature = *m_signature->template as<FunctionSignature>();
if (signature.numVectors() || signature.numReturnVectors())
m_context.notifyFunctionUsesSIMD();
@@ -500,7 +500,7 @@ auto FunctionParser<Context>::parseConstantExpression() -> Result
template<typename Context>
auto FunctionParser<Context>::parseBody() -> PartialResult
{
- m_controlStack.append({ { }, { }, 0, m_context.addTopLevel({ m_signature.as<FunctionSignature>(), nullptr }) });
+ m_controlStack.append({ { }, { }, 0, m_context.addTopLevel({ m_signature->template as<FunctionSignature>(), nullptr }) });
uint8_t op = 0;
while (m_controlStack.size()) {
m_currentOpcodeStartingOffset = m_offset;
@@ -540,7 +540,7 @@ auto FunctionParser<Context>::parseBody() -> PartialResult
WASM_FAIL_IF_HELPER_FAILS(parseExpression());
m_context.didParseOpcode();
}
- WASM_FAIL_IF_HELPER_FAILS(m_context.endTopLevel({ m_signature.as<FunctionSignature>(), nullptr }, m_expressionStack));
+ WASM_FAIL_IF_HELPER_FAILS(m_context.endTopLevel({ m_signature->template as<FunctionSignature>(), nullptr }, m_expressionStack));
if (Context::validateFunctionBodySize)
WASM_PARSER_FAIL_IF(m_offset != source().size(), "function body size doesn't match the expected size");
@@ -3067,7 +3067,7 @@ FOR_EACH_WASM_MEMORY_STORE_OP(CREATE_CASE)
if (m_currentOpcode == TailCall) {
- const auto& callerSignature = *m_signature.as<FunctionSignature>();
+ const auto& callerSignature = *m_signature->template as<FunctionSignature>();
WASM_PARSER_FAIL_IF(calleeSignature.returnCount() != callerSignature.returnCount(), "tail call function index "_s, functionIndex, " with return count "_s, calleeSignature.returnCount(), ", but the caller's signature has "_s, callerSignature.returnCount(), " return values"_s);
@@ -3135,7 +3135,7 @@ FOR_EACH_WASM_MEMORY_STORE_OP(CREATE_CASE)
if (m_currentOpcode == TailCallIndirect) {
- const auto& callerSignature = *m_signature.as<FunctionSignature>();
+ const auto& callerSignature = *m_signature->template as<FunctionSignature>();
WASM_PARSER_FAIL_IF(calleeSignature.returnCount() != callerSignature.returnCount(), "tail call indirect function with return count "_s, calleeSignature.returnCount(), "_s, but the caller's signature has "_s, callerSignature.returnCount(), " return values"_s);
@@ -3200,7 +3200,7 @@ FOR_EACH_WASM_MEMORY_STORE_OP(CREATE_CASE)
ResultList results;
if (m_currentOpcode == TailCallRef) {
- const auto& callerSignature = *m_signature.as<FunctionSignature>();
+ const auto& callerSignature = *m_signature->template as<FunctionSignature>();
WASM_PARSER_FAIL_IF(calleeSignature.returnCount() != callerSignature.returnCount(), "tail call indirect function with return count "_s, calleeSignature.returnCount(), "_s, but the caller's signature has "_s, callerSignature.returnCount(), " return values"_s);
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to memory corruption
Researchers: Google Big Sleep
Background
Advisory: Apple Security Advisory
| Attribute | Value |
|---|---|
| CVE | CVE-2025-43433 |
| Bugzilla | 298093 |
| Component | WebKit |
| Bug Class | LogicError |
| Severity | medium |
| Advisory | Apple Advisory |
Fix not public. The WebKit fix for this bug is embargoed or not yet disclosed. The bug ID is confirmed from the Apple advisory, but no public commit references it.
Root Cause Analysis
The issue was addressed with improved memory handling.
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: Google Big Sleep
Key insight. WasmTable/WasmGlobal stored a reference type by TypeIndex without retaining its TypeDefinition, so the definition could be freed while the object lived on — a dangling-TypeIndex UAF; retaining it via TypeInformation::getRef fixes it.
Patch Details
diff --git a/Source/JavaScriptCore/wasm/WasmGlobal.h b/Source/JavaScriptCore/wasm/WasmGlobal.h
index d6e7029f402a..970a04f88133 100644
--- a/Source/JavaScriptCore/wasm/WasmGlobal.h
+++ b/Source/JavaScriptCore/wasm/WasmGlobal.h
@@ -97,6 +97,7 @@ public:
private:
Global(Wasm::Type type, Wasm::Mutability mutability, uint64_t initialValue)
: m_type(type)
+ , m_typeDefinition(TypeInformation::getRef(type.index))
, m_mutability(mutability)
{
ASSERT(m_type != Types::V128);
@@ -105,6 +106,7 @@ private:
Global(Wasm::Type type, Wasm::Mutability mutability, v128_t initialValue)
: m_type(type)
+ , m_typeDefinition(TypeInformation::getRef(type.index))
, m_mutability(mutability)
{
ASSERT(m_type == Types::V128);
@@ -112,6 +114,8 @@ private:
}
Wasm::Type m_type;
+ // If m_type came from a TypeDefinition, the following retains the definition to prevent a dangling m_type.
+ RefPtr<const Wasm::TypeDefinition> m_typeDefinition;
Wasm::Mutability m_mutability;
JSWebAssemblyGlobal* m_owner { nullptr };
Value m_value;
diff --git a/Source/JavaScriptCore/wasm/WasmTable.cpp b/Source/JavaScriptCore/wasm/WasmTable.cpp
index 535b107053f9..be6b6df2e054 100644
--- a/Source/JavaScriptCore/wasm/WasmTable.cpp
+++ b/Source/JavaScriptCore/wasm/WasmTable.cpp
@@ -86,6 +86,7 @@ Table::Table(uint32_t initial, std::optional<uint32_t> maximum, Type wasmType, T
: m_maximum(maximum)
, m_type(type)
, m_wasmType(wasmType)
+ , m_wasmTypeDefinition(TypeInformation::getRef(wasmType.index))
, m_isFixedSized(maximum && maximum.value() == initial)
, m_owner(nullptr)
{
diff --git a/Source/JavaScriptCore/wasm/WasmTable.h b/Source/JavaScriptCore/wasm/WasmTable.h
index 9ab585feebde..813319e03a19 100644
--- a/Source/JavaScriptCore/wasm/WasmTable.h
+++ b/Source/JavaScriptCore/wasm/WasmTable.h
@@ -104,6 +104,8 @@ protected:
NO_UNIQUE_ADDRESS const std::optional<uint32_t> m_maximum;
const TableElementType m_type;
Type m_wasmType;
+ // If m_wasmType came from a TypeDefinition, the following retains the definition to prevent a dangling m_wasmType.
+ RefPtr<const TypeDefinition> m_wasmTypeDefinition;
bool m_isFixedSized { false };
JSWebAssemblyTable* m_owner;
};
diff --git a/Source/JavaScriptCore/wasm/WasmTypeDefinition.h b/Source/JavaScriptCore/wasm/WasmTypeDefinition.h
index 09328c19e57b..409fd2803524 100644
--- a/Source/JavaScriptCore/wasm/WasmTypeDefinition.h
+++ b/Source/JavaScriptCore/wasm/WasmTypeDefinition.h
@@ -958,6 +958,8 @@ public:
static const TypeDefinition& get(TypeIndex);
static TypeIndex get(const TypeDefinition&);
+ // Unlike with `get`, the index passed to `getRef` may be a type or invalid, in which case a nullptr is returned.
+ static RefPtr<const TypeDefinition> getRef(TypeIndex);
inline static const FunctionSignature& getFunctionSignature(TypeIndex);
inline static std::optional<const FunctionSignature*> tryGetFunctionSignature(TypeIndex);
diff --git a/Source/JavaScriptCore/wasm/WasmTypeDefinitionInlines.h b/Source/JavaScriptCore/wasm/WasmTypeDefinitionInlines.h
index 1c8329d17afd..672a8201c5af 100644
--- a/Source/JavaScriptCore/wasm/WasmTypeDefinitionInlines.h
+++ b/Source/JavaScriptCore/wasm/WasmTypeDefinitionInlines.h
@@ -83,6 +83,13 @@ inline TypeIndex TypeInformation::get(const TypeDefinition& type)
return type.index();
}
+inline RefPtr<const TypeDefinition> TypeInformation::getRef(TypeIndex typeIndex)
+{
+ if (typeIndexIsType(typeIndex) || typeIndex == TypeDefinition::invalidIndex)
+ return nullptr;
+ return TypeInformation::get(typeIndex);
+}
+
} } // namespace JSC::Wasm
#endif // ENABLE(WEBASSEMBLY)
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: Justin Cohen of Google
Key insight. The player layer view was destroyed without first detaching and releasing its videoView subview, leaving a dangling reference during media/fullscreen teardown; ordering the subview’s removal and release at the start of dealloc closes the use-after-free window.
Patch Details
diff --git a/Source/WebCore/platform/cocoa/WebAVPlayerLayerView.mm b/Source/WebCore/platform/cocoa/WebAVPlayerLayerView.mm
index 925994716aa1..509b492a0654 100644
--- a/Source/WebCore/platform/cocoa/WebAVPlayerLayerView.mm
+++ b/Source/WebCore/platform/cocoa/WebAVPlayerLayerView.mm
@@ -158,6 +158,11 @@ static void WebAVPlayerLayerView_dealloc(id aSelf, SEL)
{
WebAVPlayerLayerView *playerLayerView = aSelf;
+ RetainPtr<UIView> videoView = playerLayerView.videoView;
+ [playerLayerView setVideoView:nil];
+ [videoView removeFromSuperview];
+ videoView = nil;
+
#if HAVE(PICTUREINPICTUREPLAYERLAYERVIEW)
[playerLayerView setValue:nil forKey:pictureInPicturePlayerLayerViewKey];
#endif
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: rheza (@ginggilBesel), shandikri working with Trend Micro Zero Day Initiative
Key insight. Map and Set iterators shared the SpecObjectOther speculated type, so the DFG/FTL couldn’t distinguish them and a speculation for one accepted the other – an iterator type confusion; giving each a distinct SpeculatedType makes the checks precise.
Patch Details
diff --git a/JSTests/stress/map-set-iterator-speculated-types.js b/JSTests/stress/map-set-iterator-speculated-types.js
new file mode 100644
index 000000000000..32cdfcc67c97
--- /dev/null
+++ b/JSTests/stress/map-set-iterator-speculated-types.js
@@ -0,0 +1,10 @@
+function test() {
+ const map = new Map();
+ var iterator = map[Symbol.iterator]();
+ const proto = Object.getPrototypeOf(iterator);
+ try{
+ map.get(...map, ...proto);
+ }catch(e){}
+}
+for (var i=0;i<testLoopCount;i++)
+ test();
diff --git a/Source/JavaScriptCore/bytecode/SpeculatedType.cpp b/Source/JavaScriptCore/bytecode/SpeculatedType.cpp
index 33f9ba011188..5c34416860c6 100644
--- a/Source/JavaScriptCore/bytecode/SpeculatedType.cpp
+++ b/Source/JavaScriptCore/bytecode/SpeculatedType.cpp
@@ -731,8 +731,9 @@ std::optional<SpeculatedType> speculationFromJSType(JSType type)
case DataViewType:
return SpecDataViewObject;
case JSMapIteratorType:
+ return SpecMapIteratorObject;
case JSSetIteratorType:
- return SpecObjectOther;
+ return SpecSetIteratorObject;
default:
return std::nullopt;
}
diff --git a/Source/JavaScriptCore/bytecode/SpeculatedType.h b/Source/JavaScriptCore/bytecode/SpeculatedType.h
index 0d8e1ce0c25b..86ff82a8c8ac 100644
--- a/Source/JavaScriptCore/bytecode/SpeculatedType.h
+++ b/Source/JavaScriptCore/bytecode/SpeculatedType.h
@@ -63,33 +63,35 @@ static constexpr SpeculatedType SpecDateObject = 1ull <<
static constexpr SpeculatedType SpecPromiseObject = 1ull << 21; // It's definitely a Promise object or one of its subclasses.
static constexpr SpeculatedType SpecMapObject = 1ull << 22; // It's definitely a Map object or one of its subclasses.
static constexpr SpeculatedType SpecSetObject = 1ull << 23; // It's definitely a Set object or one of its subclasses.
-static constexpr SpeculatedType SpecWeakMapObject = 1ull << 24; // It's definitely a WeakMap object or one of its subclasses.
-static constexpr SpeculatedType SpecWeakSetObject = 1ull << 25; // It's definitely a WeakSet object or one of its subclasses.
-static constexpr SpeculatedType SpecProxyObject = 1ull << 26; // It's definitely a Proxy object or one of its subclasses.
-static constexpr SpeculatedType SpecGlobalProxy = 1ull << 27; // It's definitely a Proxy object or one of its subclasses.
-static constexpr SpeculatedType SpecDerivedArray = 1ull << 28; // It's definitely a DerivedArray object.
-static constexpr SpeculatedType SpecObjectOther = 1ull << 29; // It's definitely an object but not JSFinalObject, JSArray, or JSFunction.
-static constexpr SpeculatedType SpecStringIdent = 1ull << 30; // It's definitely a JSString, and it's an identifier.
-static constexpr SpeculatedType SpecStringResolvedVar = 1ull << 31; // It's definitely a JSString, and it's not an identifier. And string is resolved.
-static constexpr SpeculatedType SpecStringUnresolvedVar = 1ull << 32; // It's definitely a JSString, and it's not an identifier. And string is unresolved.
+static constexpr SpeculatedType SpecMapIteratorObject = 1ull << 24; // It's definitely a Map iterator object or one of its subclasses.
+static constexpr SpeculatedType SpecSetIteratorObject = 1ull << 25; // It's definitely a Set iterator object or one of its subclasses.
+static constexpr SpeculatedType SpecWeakMapObject = 1ull << 26; // It's definitely a WeakMap object or one of its subclasses.
+static constexpr SpeculatedType SpecWeakSetObject = 1ull << 27; // It's definitely a WeakSet object or one of its subclasses.
+static constexpr SpeculatedType SpecProxyObject = 1ull << 28; // It's definitely a Proxy object or one of its subclasses.
+static constexpr SpeculatedType SpecGlobalProxy = 1ull << 29; // It's definitely a Proxy object or one of its subclasses.
+static constexpr SpeculatedType SpecDerivedArray = 1ull << 30; // It's definitely a DerivedArray object.
+static constexpr SpeculatedType SpecObjectOther = 1ull << 31; // It's definitely an object but not JSFinalObject, JSArray, or JSFunction.
+static constexpr SpeculatedType SpecStringIdent = 1ull << 32; // It's definitely a JSString, and it's an identifier.
+static constexpr SpeculatedType SpecStringResolvedVar = 1ull << 33; // It's definitely a JSString, and it's not an identifier. And string is resolved.
+static constexpr SpeculatedType SpecStringUnresolvedVar = 1ull << 34; // It's definitely a JSString, and it's not an identifier. And string is unresolved.
static constexpr SpeculatedType SpecStringVar = SpecStringUnresolvedVar | SpecStringResolvedVar; // It's definitely a JSString, and it's not an identifier.
static constexpr SpeculatedType SpecStringResolved = SpecStringIdent | SpecStringResolvedVar; // It's definitely a JSString, and it's resolved. May be an identifier or not.
static constexpr SpeculatedType SpecString = SpecStringIdent | SpecStringVar; // It's definitely a JSString.
-static constexpr SpeculatedType SpecSymbol = 1ull << 33; // It's definitely a Symbol.
-static constexpr SpeculatedType SpecCellOther = 1ull << 34; // It's definitely a JSCell but not a subclass of JSObject and definitely not a JSString, BigInt, or Symbol.
-static constexpr SpeculatedType SpecBoolInt32 = 1ull << 35; // It's definitely an Int32 with value 0 or 1.
-static constexpr SpeculatedType SpecNonBoolInt32 = 1ull << 36; // It's definitely an Int32 with value other than 0 or 1.
+static constexpr SpeculatedType SpecSymbol = 1ull << 35; // It's definitely a Symbol.
+static constexpr SpeculatedType SpecCellOther = 1ull << 36; // It's definitely a JSCell but not a subclass of JSObject and definitely not a JSString, BigInt, or Symbol.
+static constexpr SpeculatedType SpecBoolInt32 = 1ull << 37; // It's definitely an Int32 with value 0 or 1.
+static constexpr SpeculatedType SpecNonBoolInt32 = 1ull << 38; // It's definitely an Int32 with value other than 0 or 1.
static constexpr SpeculatedType SpecInt32Only = SpecBoolInt32 | SpecNonBoolInt32; // It's definitely an Int32.
-static constexpr SpeculatedType SpecInt32AsInt52 = 1ull << 37; // It's an Int52 and it can fit in an int32.
-static constexpr SpeculatedType SpecNonInt32AsInt52 = 1ull << 38; // It's an Int52 and it can't fit in an int32.
+static constexpr SpeculatedType SpecInt32AsInt52 = 1ull << 39; // It's an Int52 and it can fit in an int32.
+static constexpr SpeculatedType SpecNonInt32AsInt52 = 1ull << 40; // It's an Int52 and it can't fit in an int32.
static constexpr SpeculatedType SpecInt52Any = SpecInt32AsInt52 | SpecNonInt32AsInt52; // It's any kind of Int52.
-static constexpr SpeculatedType SpecAnyIntAsDouble = 1ull << 39; // It's definitely an Int52 and it's inside a double.
-static constexpr SpeculatedType SpecNonIntAsDouble = 1ull << 40; // It's definitely not an Int52 but it's a real number and it's a double.
+static constexpr SpeculatedType SpecAnyIntAsDouble = 1ull << 41; // It's definitely an Int52 and it's inside a double.
+static constexpr SpeculatedType SpecNonIntAsDouble = 1ull << 42; // It's definitely not an Int52 but it's a real number and it's a double.
static constexpr SpeculatedType SpecDoubleReal = SpecNonIntAsDouble | SpecAnyIntAsDouble; // It's definitely a non-NaN double.
-static constexpr SpeculatedType SpecDoublePureNaN = 1ull << 41; // It's definitely a NaN that is safe to tag (i.e. pure).
-static constexpr SpeculatedType SpecDoubleImpureNaN = 1ull << 42; // It's definitely a NaN that is unsafe to tag (i.e. impure).
+static constexpr SpeculatedType SpecDoublePureNaN = 1ull << 43; // It's definitely a NaN that is safe to tag (i.e. pure).
+static constexpr SpeculatedType SpecDoubleImpureNaN = 1ull << 44; // It's definitely a NaN that is unsafe to tag (i.e. impure).
static constexpr SpeculatedType SpecDoubleNaN = SpecDoublePureNaN | SpecDoubleImpureNaN; // It's definitely some kind of NaN.
static constexpr SpeculatedType SpecBytecodeDouble = SpecDoubleReal | SpecDoublePureNaN; // It's either a non-NaN or a NaN double, but it's definitely not impure NaN.
static constexpr SpeculatedType SpecFullDouble = SpecDoubleReal | SpecDoubleNaN; // It's either a non-NaN or a NaN double.
@@ -99,21 +101,21 @@ static constexpr SpeculatedType SpecBytecodeNumber = SpecInt3
static constexpr SpeculatedType SpecIntAnyFormat = SpecInt52Any | SpecInt32Only | SpecAnyIntAsDouble;
static constexpr SpeculatedType SpecFullNumber = SpecIntAnyFormat | SpecFullDouble; // It's either an Int32, Int52, or a Double, and the Double can be impure NaN.
-static constexpr SpeculatedType SpecBoolean = 1ull << 43; // It's definitely a Boolean.
-static constexpr SpeculatedType SpecOther = 1ull << 44; // It's definitely either Null or Undefined.
+static constexpr SpeculatedType SpecBoolean = 1ull << 45; // It's definitely a Boolean.
+static constexpr SpeculatedType SpecOther = 1ull << 46; // It's definitely either Null or Undefined.
static constexpr SpeculatedType SpecMisc = SpecBoolean | SpecOther; // It's definitely either a boolean, Null, or Undefined.
-static constexpr SpeculatedType SpecEmpty = 1ull << 45; // It's definitely an empty value marker.
-static constexpr SpeculatedType SpecHeapBigInt = 1ull << 46; // It's definitely a BigInt that is allocated on the heap
-static constexpr SpeculatedType SpecBigInt32 = 1ull << 47; // It's definitely a small BigInt that is inline the JSValue
+static constexpr SpeculatedType SpecEmpty = 1ull << 47; // It's definitely an empty value marker.
+static constexpr SpeculatedType SpecHeapBigInt = 1ull << 48; // It's definitely a BigInt that is allocated on the heap
+static constexpr SpeculatedType SpecBigInt32 = 1ull << 49; // It's definitely a small BigInt that is inline the JSValue
#if USE(BIGINT32)
static constexpr SpeculatedType SpecBigInt = SpecBigInt32 | SpecHeapBigInt;
#else
// We should not include SpecBigInt32. We are using SpecBigInt in various places like prediction. If this includes SpecBigInt32, fixup phase is confused if !USE(BIGINT32) since it is not using AnyBigIntUse.
static constexpr SpeculatedType SpecBigInt = SpecHeapBigInt;
#endif
-static constexpr SpeculatedType SpecDataViewObject = 1ull << 48; // It's definitely a JSDataView.
+static constexpr SpeculatedType SpecDataViewObject = 1ull << 50; // It's definitely a JSDataView.
static constexpr SpeculatedType SpecPrimitive = SpecString | SpecSymbol | SpecBytecodeNumber | SpecMisc | SpecBigInt; // It's any non-Object JSValue.
-static constexpr SpeculatedType SpecObject = SpecFinalObject | SpecArray | SpecFunction | SpecTypedArrayView | SpecDirectArguments | SpecScopedArguments | SpecStringObject | SpecRegExpObject | SpecDateObject | SpecPromiseObject | SpecMapObject | SpecSetObject | SpecWeakMapObject | SpecWeakSetObject | SpecProxyObject | SpecGlobalProxy | SpecDerivedArray | SpecObjectOther | SpecDataViewObject; // Bitmask used for testing for any kind of object prediction.
+static constexpr SpeculatedType SpecObject = SpecFinalObject | SpecArray | SpecFunction | SpecTypedArrayView | SpecDirectArguments | SpecScopedArguments | SpecStringObject | SpecRegExpObject | SpecDateObject | SpecPromiseObject | SpecMapObject | SpecSetObject | SpecMapIteratorObject | SpecSetIteratorObject | SpecWeakMapObject | SpecWeakSetObject | SpecProxyObject | SpecGlobalProxy | SpecDerivedArray | SpecObjectOther | SpecDataViewObject; // Bitmask used for testing for any kind of object prediction.
static constexpr SpeculatedType SpecCell = SpecObject | SpecString | SpecSymbol | SpecCellOther | SpecHeapBigInt; // It's definitely a JSCell.
static constexpr SpeculatedType SpecHeapTop = SpecCell | SpecBigInt32 | SpecBytecodeNumber | SpecMisc; // It can be any of the above, except for SpecInt52Only and SpecDoubleImpureNaN.
static constexpr SpeculatedType SpecBytecodeTop = SpecHeapTop | SpecEmpty; // It can be any of the above, except for SpecInt52Only and SpecDoubleImpureNaN. Corresponds to what could be found in a bytecode local.
diff --git a/Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp b/Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp
index e830d584a068..30e83c2f2aa2 100644
--- a/Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp
+++ b/Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp
@@ -11887,12 +11887,12 @@ void SpeculativeJIT::speculateSetObject(Edge edge)
void SpeculativeJIT::speculateMapIteratorObject(Edge edge, GPRReg cell)
{
- speculateCellType(edge, cell, SpecObjectOther, JSMapIteratorType);
+ speculateCellType(edge, cell, SpecMapIteratorObject, JSMapIteratorType);
}
void SpeculativeJIT::speculateMapIteratorObject(Edge edge)
{
- if (!needsTypeCheck(edge, SpecObjectOther))
+ if (!needsTypeCheck(edge, SpecMapIteratorObject))
return;
SpeculateCellOperand operand(this, edge);
@@ -11901,12 +11901,12 @@ void SpeculativeJIT::speculateMapIteratorObject(Edge edge)
void SpeculativeJIT::speculateSetIteratorObject(Edge edge, GPRReg cell)
{
- speculateCellType(edge, cell, SpecObjectOther, JSSetIteratorType);
+ speculateCellType(edge, cell, SpecSetIteratorObject, JSSetIteratorType);
}
void SpeculativeJIT::speculateSetIteratorObject(Edge edge)
{
- if (!needsTypeCheck(edge, SpecObjectOther))
+ if (!needsTypeCheck(edge, SpecSetIteratorObject))
return;
SpeculateCellOperand operand(this, edge);
diff --git a/Source/JavaScriptCore/dfg/DFGUseKind.h b/Source/JavaScriptCore/dfg/DFGUseKind.h
index 09c8f8e863a4..edacb1f0641c 100644
--- a/Source/JavaScriptCore/dfg/DFGUseKind.h
+++ b/Source/JavaScriptCore/dfg/DFGUseKind.h
@@ -178,8 +178,9 @@ inline SpeculatedType typeFilterFor(UseKind useKind)
case SetObjectUse:
return SpecSetObject;
case MapIteratorObjectUse:
+ return SpecMapIteratorObject;
case SetIteratorObjectUse:
- return SpecObjectOther;
+ return SpecSetIteratorObject;
case WeakMapObjectUse:
return SpecWeakMapObject;
case WeakSetObjectUse:
diff --git a/Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp b/Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp
index 1561f29e7e15..4c7c38a8ec43 100644
--- a/Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp
+++ b/Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp
@@ -24030,7 +24030,7 @@ IGNORE_CLANG_WARNINGS_END
void speculateMapIteratorObject(Edge edge, LValue cell)
{
FTL_TYPE_CHECK(
- jsValueValue(cell), edge, SpecObjectOther, isNotType(cell, JSMapIteratorType));
+ jsValueValue(cell), edge, SpecMapIteratorObject, isNotType(cell, JSMapIteratorType));
}
void speculateMapIteratorObject(Edge edge)
@@ -24052,7 +24052,7 @@ IGNORE_CLANG_WARNINGS_END
void speculateSetIteratorObject(Edge edge, LValue cell)
{
FTL_TYPE_CHECK(
- jsValueValue(cell), edge, SpecObjectOther, isNotType(cell, JSSetIteratorType));
+ jsValueValue(cell), edge, SpecSetIteratorObject, isNotType(cell, JSSetIteratorType));
}
void speculateSetIteratorObject(Edge edge)
diff --git a/Source/JavaScriptCore/runtime/JSType.h b/Source/JavaScriptCore/runtime/JSType.h
index a77d5e41a525..5c3f9907df6e 100644
--- a/Source/JavaScriptCore/runtime/JSType.h
+++ b/Source/JavaScriptCore/runtime/JSType.h
@@ -127,8 +127,8 @@ namespace JSC {
macro(JSArrayIteratorType, SpecObjectOther) \
macro(JSIteratorType, SpecObjectOther) \
macro(JSIteratorHelperType, SpecObjectOther) \
- macro(JSMapIteratorType, SpecObjectOther) \
- macro(JSSetIteratorType, SpecObjectOther) \
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: Nan Wang (@eternalsakura13)
Key insight. DFG optimization phases must only trust abstract state for blocks the CFA actually reached; constant-folding a block with cfaHasVisited == false reasons over uninitialized abstract values and corrupts the IR.
Patch Details
diff --git a/Source/JavaScriptCore/dfg/DFGConstantFoldingPhase.cpp b/Source/JavaScriptCore/dfg/DFGConstantFoldingPhase.cpp
index 3b6f1d6efd6d..42dd2369f8d4 100644
--- a/Source/JavaScriptCore/dfg/DFGConstantFoldingPhase.cpp
+++ b/Source/JavaScriptCore/dfg/DFGConstantFoldingPhase.cpp
@@ -114,6 +114,10 @@ public:
private:
bool foldConstants(BasicBlock* block)
{
+ // CFAUnreachable, skip
+ if (!block->cfaHasVisited)
+ return false;
+
bool changed = false;
m_state.beginBasicBlock(block);
for (unsigned indexInBlock = 0; indexInBlock < block->size(); ++indexInBlock) {
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: rheza (@ginggilBesel)
Key insight. The scratch register is only safe to enable once no input value lives in it: the bug was holding AllowMacroScratchRegisterUsage across regions where the callee pointer or a tail-call argument still occupied the scratch/tmp register, so an internal spill silently destroyed live state.
Patch Details
diff --git a/JSTests/wasm/stress/omg-tail-call-clobber-scratch-register.js b/JSTests/wasm/stress/omg-tail-call-clobber-scratch-register.js
new file mode 100644
index 000000000000..e3878b1353e3
--- /dev/null
+++ b/JSTests/wasm/stress/omg-tail-call-clobber-scratch-register.js
@@ -0,0 +1,33 @@
+//@ runDefault("--jitPolicyScale=0")
+const v39 = new WebAssembly.Instance(new WebAssembly.Module(new Uint8Array([
+ 0x00, 0x61, 0x73, 0x6D, 0x01, 0x00, 0x00, 0x00, 0x01, 0x23,
+ 0x03, 0x60, 0x00, 0x00, 0x60, 0x09, 0x7E, 0x63, 0x6C, 0x7E,
+ 0x7F, 0x63, 0x6F, 0x7E, 0x63, 0x71, 0x63, 0x6F, 0x7E, 0x03,
+ 0x7E, 0x63, 0x71, 0x63, 0x72, 0x60, 0x01, 0x63, 0x6C, 0x03,
+ 0x7E, 0x63, 0x71, 0x63, 0x72, 0x03, 0x03, 0x02, 0x01, 0x02,
+ 0x04, 0x05, 0x01, 0x63, 0x70, 0x00, 0x0A, 0x05, 0x01, 0x00,
+ 0x06, 0x01, 0x00, 0x07, 0x11, 0x03, 0x02, 0x77, 0x30, 0x00,
+ 0x00, 0x02, 0x77, 0x31, 0x00, 0x01, 0x03, 0x77, 0x74, 0x30,
+ 0x01, 0x00, 0x09, 0x09, 0x01, 0x02, 0x00, 0x41, 0x00, 0x0B,
+ 0x00, 0x01, 0x00, 0x00, 0x20, 0x19, 0x6D, 0x65, 0x74, 0x61,
+ 0x64, 0x61, 0x74, 0x61, 0x2E, 0x63, 0x6F, 0x64, 0x65, 0x2E,
+ 0x62, 0x72, 0x61, 0x6E, 0x63, 0x68, 0x5F, 0x68, 0x69, 0x6E,
+ 0x74, 0x01, 0x01, 0x01, 0x30, 0x01, 0x01, 0x0A, 0x92, 0x01,
+ 0x02, 0x1D, 0x03, 0x01, 0x7B, 0x01, 0x7E, 0x01, 0x63, 0x72,
+ 0x20, 0x00, 0xFD, 0x12, 0x21, 0x09, 0x42, 0x07, 0x21, 0x0A,
+ 0xD0, 0x72, 0x21, 0x0B, 0x20, 0x0A, 0x20, 0x06, 0x20, 0x0B,
+ 0x0B, 0x72, 0x0A, 0x01, 0x7F, 0x01, 0x7F, 0x01, 0x7F, 0x01,
+ 0x7F, 0x01, 0x63, 0x72, 0x01, 0x63, 0x71, 0x01, 0x7E, 0x01,
+ 0x7E, 0x01, 0x7E, 0x01, 0x7F, 0x41, 0x94, 0xFD, 0x01, 0x21,
+ 0x01, 0x03, 0x00, 0x41, 0x01, 0x21, 0x02, 0x20, 0x01, 0x20,
+ 0x02, 0x6B, 0x21, 0x03, 0x20, 0x03, 0x21, 0x01, 0x20, 0x01,
+ 0x0D, 0x00, 0x0B, 0x41, 0x00, 0x21, 0x04, 0xD0, 0x72, 0x21,
+ 0x05, 0xD0, 0x71, 0x21, 0x06, 0x42, 0xFF, 0x00, 0x21, 0x07,
+ 0x42, 0xE7, 0xFF, 0xFF, 0xFF, 0x01, 0x21, 0x08, 0x42, 0x09,
+ 0x21, 0x09, 0x41, 0x00, 0x21, 0x0A, 0x20, 0x09, 0x20, 0x00,
+ 0x20, 0x08, 0x20, 0x04, 0x20, 0x05, 0x20, 0x07, 0x20, 0x06,
+ 0x20, 0x05, 0x20, 0x08, 0x20, 0x0A, 0x13, 0x01, 0x00, 0x20,
+ 0x09, 0x20, 0x06, 0x20, 0x05, 0x0B,
+])));
+// Don't crash.
+v39.exports.w1(-900879769);
diff --git a/Source/JavaScriptCore/wasm/WasmOMGIRGenerator.cpp b/Source/JavaScriptCore/wasm/WasmOMGIRGenerator.cpp
index 00d08fda5902..f3ed7363c49c 100644
--- a/Source/JavaScriptCore/wasm/WasmOMGIRGenerator.cpp
+++ b/Source/JavaScriptCore/wasm/WasmOMGIRGenerator.cpp
@@ -1847,9 +1847,10 @@ auto OMGIRGenerator::emitIndirectCall(Value* calleeInstance, Value* calleeCode,
patchpoint->append(boxedCalleeCallee, ValueRep::SomeRegister);
patchArgsIndex += m_proc.resultCount(patchpoint->type());
patchpoint->setGenerator([prepareForCall = prepareForCall, patchArgsIndex](CCallHelpers& jit, const B3::StackmapGenerationParams& params) {
- AllowMacroScratchRegisterUsage allowScratch(jit);
prepareForCall->run(jit, params);
jit.storeWasmCalleeToCalleeCallFrame(params[patchArgsIndex + 1].gpr(), sizeof(CallerFrameAndPC) - prologueStackPointerDelta());
+ // Allow scratch after the callee is stored, which could be in the scratch register.
+ AllowMacroScratchRegisterUsage allowScratch(jit);
jit.farJump(params[patchArgsIndex].gpr(), WasmEntryPtrTag);
});
return { };
@@ -5542,33 +5543,74 @@ static inline void prepareForTailCallImpl(unsigned functionIndex, CCallHelpers&
auto& functionSignature = *signature.as<FunctionSignature>();
const Checked<int32_t> offsetOfFirstSlotFromFP = WTF::roundUpToMultipleOf<stackAlignmentBytes()>(wasmCallerInfoAsCallee.headerAndArgumentStackSizeInBytes);
JIT_COMMENT(jit, "Set up tail call, new FP offset from FP: ", newFPOffsetFromFP);
- AllowMacroScratchRegisterUsage allowScratch(jit);
+
+ const unsigned frameSize = params.code().frameSize();
+ ASSERT(WTF::roundUpToMultipleOf<stackAlignmentBytes()>(frameSize) == frameSize);
+ ASSERT(WTF::roundUpToMultipleOf<stackAlignmentBytes()>(std::abs(newFPOffsetFromFP)) == static_cast<size_t>(std::abs(newFPOffsetFromFP)));
+
+ auto fpOffsetToSPOffset = [frameSize](int32_t offset) {
+ return checkedSum<int>(safeCast<int>(frameSize), offset).value();
+ };
+
+ auto newReturnPCOffset = fpOffsetToSPOffset(checkedSum<intptr_t>(CallFrame::returnPCOffset(), newFPOffsetFromFP).value());
+
+ // We requested some extra stack space below via requestCallArgAreaSize
+ // ... FP [initial safe area][caller stack space ] [callArgSpace ] SP ...
+ // becomes
+ // ... FP [safe area growing -> ] [danger ] [ scratch ] SP ...
+ // This scratch space sits at the very bottom of the stack, near sp.
+ // AirLowerStackArgs takes care of adding callArgSpace to our total caller frame size.
+ // BUT, even though we have this extra space, the new frame might be bigger, so we can't
+ // use the new frame as scratch. The new return pc represents the lowest offset from SP we can use.
+ int spillPointer = 0;
+ const int scratchAreaUpperBound = std::min(
+ safeCast<int>(WTF::roundUpToMultipleOf<stackAlignmentBytes()>(static_cast<int>(wasmCalleeInfoAsCallee.headerAndArgumentStackSizeInBytes))),
+ newReturnPCOffset);
+ auto allocateSpill = [&] (Width width) -> int {
+ int offset = spillPointer;
+ spillPointer += bytesForWidth(width);
+ ASSERT(spillPointer <= scratchAreaUpperBound);
+ ASSERT(offset < scratchAreaUpperBound);
+ return offset;
+ };
+
+ RegisterAtOffsetList calleeSaves = params.code().calleeSaveRegisterAtOffsetList();
// Be careful not to clobber this below.
// We also need to make sure that we preserve this if it is used by the patchpoint body.
- bool clobbersTmp = false;
+ AllowMacroScratchRegisterUsage allowScratch(jit);
auto tmp = jit.scratchRegister();
- int tmpSpill = 0;
+ bool tmpNeedsSaving = false;
+ int tmpSpillOffsetRelativeToOriginalSP = 0;
- // Set up a valid frame so that we can clobber this one.
- RegisterAtOffsetList calleeSaves = params.code().calleeSaveRegisterAtOffsetList();
- jit.emitRestore(calleeSaves);
-
- for (unsigned i = 0; i < params.size(); ++i) {
- auto arg = params[i];
- if (arg.isGPR()) {
- ASSERT(!calleeSaves.find(arg.gpr()));
- if (arg.gpr() == tmp)
- clobbersTmp = true;
- continue;
- }
- if (arg.isFPR()) {
- ASSERT(!calleeSaves.find(arg.fpr()));
- continue;
+ // Nothing before saving tmp can use the scratch register since it might clobber an input.
+ {
+ DisallowMacroScratchRegisterUsage disallowScratch(jit);
+
+ // Set up a valid frame so that we can clobber this one.
+ jit.emitRestore(calleeSaves);
+
+ for (unsigned i = 0; i < params.size(); ++i) {
+ auto arg = params[i];
+ if (arg.isGPR()) {
+ ASSERT(!calleeSaves.find(arg.gpr()));
+ if (arg.gpr() == tmp)
+ tmpNeedsSaving = true;
+ continue;
+ }
+ if (arg.isFPR()) {
+ ASSERT(!calleeSaves.find(arg.fpr()));
+ continue;
+ }
}
+
+ ASSERT(!calleeSaves.find(tmp));
}
- ASSERT(!calleeSaves.find(tmp));
+ if (tmpNeedsSaving) {
+ tmpSpillOffsetRelativeToOriginalSP = allocateSpill(WidthPtr);
+ jit.storePtr(tmp, CCallHelpers::Address(MacroAssembler::stackPointerRegister, tmpSpillOffsetRelativeToOriginalSP));
+ }
#if ASSERT_ENABLED
// Let's make sure we never rely on these slots, so we can use them for scratch in the future.
@@ -5579,14 +5621,6 @@ static inline void prepareForTailCallImpl(unsigned functionIndex, CCallHelpers&
CCallHelpers::Address(MacroAssembler::framePointerRegister, CallFrameSlot::argumentCountIncludingThis * sizeof(Register)));
#endif
- const unsigned frameSize = params.code().frameSize();
- ASSERT(WTF::roundUpToMultipleOf<stackAlignmentBytes()>(frameSize) == frameSize);
- ASSERT(WTF::roundUpToMultipleOf<stackAlignmentBytes()>(std::abs(newFPOffsetFromFP)) == static_cast<size_t>(std::abs(newFPOffsetFromFP)));
-
- auto fpOffsetToSPOffset = [frameSize](int32_t offset) {
- return checkedSum<int>(safeCast<int>(frameSize), offset).value();
- };
-
JIT_COMMENT(jit, "Let's use the caller's frame, so that we always have a valid frame.");
if (WasmOMGIRGeneratorInternal::verboseTailCalls) {
jit.probeDebugSIMD([frameSize, fpOffsetToSPOffset, newFPOffsetFromFP, signature = Ref<const TypeDefinition>(signature), wasmCalleeInfoAsCallee, firstPatchArg, lastPatchArg, params, functionIndex] (Probe::Context& context) {
@@ -5643,31 +5677,9 @@ static inline void prepareForTailCallImpl(unsigned functionIndex, CCallHelpers&
});
}
- auto newReturnPCOffset = fpOffsetToSPOffset(checkedSum<intptr_t>(CallFrame::returnPCOffset(), newFPOffsetFromFP).value());
-
JIT_COMMENT(jit, "Copy over args if needed into their final position, clobbering everything.");
// This code has a bunch of overlap with CallFrameShuffler and Shuffle in Air/BBQ
- // We requested some extra stack space below via requestCallArgAreaSize
- // ... FP [initial safe area][caller stack space ] [callArgSpace ] SP ...
- // becomes
- // ... FP [safe area growing -> ] [danger ] [ scratch ] SP ...
- // This scratch space sits at the very bottom of the stack, near sp.
- // AirLowerStackArgs takes care of adding callArgSpace to our total caller frame size.
- // BUT, even though we have this extra space, the new frame might be bigger, so we can't
- // use the new frame as scratch. The new return pc represents the lowest offset from SP we can use.
- int spillPointer = 0;
- const int scratchAreaUpperBound = std::min(
- safeCast<int>(WTF::roundUpToMultipleOf<stackAlignmentBytes()>(static_cast<int>(wasmCalleeInfoAsCallee.headerAndArgumentStackSizeInBytes))),
- newReturnPCOffset);
- auto allocateSpill = [&] (Width width) -> int {
- int offset = spillPointer;
- spillPointer += bytesForWidth(width);
- ASSERT(spillPointer <= scratchAreaUpperBound);
- ASSERT(offset < scratchAreaUpperBound);
- return offset;
- };
-
auto doMove = [&jit, tmp] (int srcOffset, int dstOffset, Width width) {
JIT_COMMENT(jit, "Do move ", srcOffset, " -> ", dstOffset);
auto src = CCallHelpers::Address(MacroAssembler::stackPointerRegister, srcOffset);
@@ -5697,11 +5709,6 @@ static inline void prepareForTailCallImpl(unsigned functionIndex, CCallHelpers&
JIT_COMMENT(jit, "SP[", safeAreaLowerBound, "] to SP[", stackUpperBound, "] form the safe portion of the stack to clobber; Scratches go from SP[0] to SP[", scratchAreaUpperBound, "].");
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: an anonymous researcher
Background
Advisory: Apple Security Advisory
| Attribute | Value |
|---|---|
| CVE | CVE-2025-43443 |
| Bugzilla | 299843 |
| Component | WebKit |
| Bug Class | LogicError |
| Severity | medium |
| Advisory | Apple Advisory |
Fix not public. The WebKit fix for this bug is embargoed or not yet disclosed. The bug ID is confirmed from the Apple advisory, but no public commit references it.
Root Cause Analysis
This issue was addressed with improved checks.
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: Gary Kwong, Hossein Lotfi (@hosselot) of Trend Micro Zero Day Initiative
Key insight. Representing a constant-size array and its butterfly as one fused DFG node hid the butterfly from allocation-sinking, GC and materialization analyses, so elimination could leave the array referencing an unmaterialized/freed butterfly; splitting it into NewButterflyWithSize + NewArrayWithButterfly makes the butterfly a first-class, correctly-tracked allocation.
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: Phil Beauvoir
Background
Advisory: Apple Security Advisory
| Attribute | Value |
|---|---|
| CVE | CVE-2025-43458 |
| Bugzilla | 296693 |
| Component | WebKit |
| Bug Class | LogicError |
| Severity | medium |
| Advisory | Apple Advisory |
Fix not public. The WebKit fix for this bug is embargoed or not yet disclosed. The bug ID is confirmed from the Apple advisory, but no public commit references it.
Root Cause Analysis
This issue was addressed through improved state management.
Overview
Impact:
Description: A malicious website may exfiltrate data cross-origin
Researchers: Aleksejs Popovs
Key insight. MediaResourceLoader failed to mark cross-origin, non-CORS media sub-resources as loaded-from-opaque-source, leaking their cross-origin data/timing; forcing those loads opaque (once the main resource has loaded) restores the boundary.
Patch Details
diff --git a/LayoutTests/http/tests/media/resources/hls/.htaccess b/LayoutTests/http/tests/media/resources/hls/.htaccess
new file mode 100644
index 000000000000..391a152f92b6
--- /dev/null
+++ b/LayoutTests/http/tests/media/resources/hls/.htaccess
@@ -0,0 +1,3 @@
+<Files playlist-with-cookie.m3u8>
+Header always set Access-Control-Allow-Origin "http://127.0.0.1:8000"
+</Files>
diff --git a/LayoutTests/http/tests/performance/performance-resource-timing-cross-origin-media-expected.txt b/LayoutTests/http/tests/performance/performance-resource-timing-cross-origin-media-expected.txt
new file mode 100644
index 000000000000..af5a27c8e2d9
--- /dev/null
+++ b/LayoutTests/http/tests/performance/performance-resource-timing-cross-origin-media-expected.txt
@@ -0,0 +1,3 @@
+http://127.0.0.1:8000/resources/js-test-pre.js
+http://localhost:8000/media/resources/hls/playlist-with-cookie.m3u8
+
diff --git a/LayoutTests/http/tests/performance/performance-resource-timing-cross-origin-media-with-cors-expected.txt b/LayoutTests/http/tests/performance/performance-resource-timing-cross-origin-media-with-cors-expected.txt
new file mode 100644
index 000000000000..4dee4ff8d4d7
--- /dev/null
+++ b/LayoutTests/http/tests/performance/performance-resource-timing-cross-origin-media-with-cors-expected.txt
@@ -0,0 +1,8 @@
+CONSOLE MESSAGE: Origin http://127.0.0.1:8000 is not allowed by Access-Control-Allow-Origin. Status code: 200
+CONSOLE MESSAGE: Origin http://127.0.0.1:8000 is not allowed by Access-Control-Allow-Origin. Status code: 200
+CONSOLE MESSAGE: Origin http://127.0.0.1:8000 is not allowed by Access-Control-Allow-Origin. Status code: 200
+CONSOLE MESSAGE: Origin http://127.0.0.1:8000 is not allowed by Access-Control-Allow-Origin. Status code: 200
+http://127.0.0.1:8000/resources/js-test-pre.js
+http://localhost:8000/media/resources/hls/playlist-with-cookie.m3u8
+http://localhost:8000/media/resources/hls/sub-playlist-with-cookie.py
+
diff --git a/LayoutTests/http/tests/performance/performance-resource-timing-cross-origin-media-with-cors.html b/LayoutTests/http/tests/performance/performance-resource-timing-cross-origin-media-with-cors.html
new file mode 100644
index 000000000000..b6d971f485b8
--- /dev/null
+++ b/LayoutTests/http/tests/performance/performance-resource-timing-cross-origin-media-with-cors.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+ if (window.testRunner) {
+ testRunner.dumpAsText()
+ testRunner.waitUntilDone();
+ }
+</script>
+<script src="../../resources/js-test-pre.js"></script>
+</head>
+<body>
+<video src="http://localhost:8000/media/resources/hls/playlist-with-cookie.m3u8" crossorigin>
+<script>
+ var entries = new Set();
+ let gotMetadata = false;
+ const expectedEntries = 3;
+ let runTest = function() {
+ for (let entry of performance.getEntriesByType('resource')) {
+ if (entries.has(entry.name))
+ continue;
+ entries.add(entry.name);
+ if (entries.size <= expectedEntries)
+ debug(entry.name);
+ }
+
+ if (gotMetadata && entries.size >= expectedEntries)
+ testRunner.notifyDone();
+ };
+ let video = document.querySelector("video");
+ setInterval(runTest, 100);
+
+ video.addEventListener("loadedmetadata", (e) => {
+ gotMetadata = true;
+ if (gotMetadata && entries.size >= expectedEntries)
+ testRunner.notifyDone();
+ });
+</script>
+</body>
+</html>
diff --git a/LayoutTests/http/tests/performance/performance-resource-timing-cross-origin-media.html b/LayoutTests/http/tests/performance/performance-resource-timing-cross-origin-media.html
new file mode 100644
index 000000000000..77e57e930353
--- /dev/null
+++ b/LayoutTests/http/tests/performance/performance-resource-timing-cross-origin-media.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+ if (window.testRunner) {
+ testRunner.dumpAsText()
+ testRunner.waitUntilDone();
+ }
+</script>
+<script src="../../resources/js-test-pre.js"></script>
+</head>
+<body>
+<video src="http://localhost:8000/media/resources/hls/playlist-with-cookie.m3u8">
+<script>
+ var entries = new Set();
+ let gotMetadata = false;
+
+ let video = document.querySelector("video");
+
+ let runTest = function() {
+ for (let entry of performance.getEntriesByType('resource')) {
+ if (entries.has(entry.name))
+ continue;
+ entries.add(entry.name);
+ if (entry.initiatorType == "video" && entry.name != video.src)
+ testFailed("Unexpected entry: " + entry.name);
+ else
+ debug(entry.name);
+ }
+
+ if (gotMetadata && entries.size)
+ testRunner.notifyDone();
+ };
+ setInterval(runTest, 100);
+
+ video.addEventListener("loadedmetadata", (e) => {
+ gotMetadata = true;
+ if (gotMetadata && entries.size)
+ testRunner.notifyDone();
+ });
+</script>
+</body>
+</html>
diff --git a/LayoutTests/platform/glib/TestExpectations b/LayoutTests/platform/glib/TestExpectations
index 9b96f1df14a3..08b1954dec2e 100644
--- a/LayoutTests/platform/glib/TestExpectations
+++ b/LayoutTests/platform/glib/TestExpectations
@@ -2907,6 +2907,8 @@ http/tests/media/modern-media-controls/time-control/1-to-10-hours.html [ Skip ]
http/tests/media/modern-media-controls/time-control/10-hours-or-more.html [ Skip ]
http/tests/media/modern-media-controls/time-control/less-than-10-minutes.html [ Skip ]
http/tests/media/modern-media-controls/time-control/10-minutes-to-1-hour.html [ Skip ]
+http/tests/performance/performance-resource-timing-cross-origin-media.html [ Skip ]
+http/tests/performance/performance-resource-timing-cross-origin-media-with-cors.html [ Skip ]
# DASH media playback support is disabled in the GStreamer ports.
platform/glib/media/media-can-play-dash.html [ Skip ]
diff --git a/LayoutTests/platform/win/TestExpectations b/LayoutTests/platform/win/TestExpectations
index 91716eac61a9..680d1957c1b9 100644
--- a/LayoutTests/platform/win/TestExpectations
+++ b/LayoutTests/platform/win/TestExpectations
@@ -859,6 +859,9 @@ webkit.org/b/255747 http/tests/security/canvas-remote-read-remote-video-redirect
http/tests/security/canvas-remote-read-remote-video-hls.html [ Skip ] # Timeout
+http/tests/performance/performance-resource-timing-cross-origin-media.html [ Skip ] # Timeout
+http/tests/performance/performance-resource-timing-cross-origin-media-with-cors.html [ Skip ] # Timeout
+
http/tests/security/clipboard/copy-paste-html-across-origin-sanitizes-html.html [ Skip ] # Failure
http/tests/security/clipboard/copy-paste-html-across-origin-strips-mso-list.html [ Skip ] # Failure
http/tests/security/clipboard/copy-paste-html-cross-origin-iframe-across-origin.html [ Skip ] # Failure
diff --git a/Source/WebCore/loader/MediaResourceLoader.cpp b/Source/WebCore/loader/MediaResourceLoader.cpp
index 10de567ac46b..efcd95c91435 100644
--- a/Source/WebCore/loader/MediaResourceLoader.cpp
+++ b/Source/WebCore/loader/MediaResourceLoader.cpp
@@ -123,6 +123,10 @@ RefPtr<PlatformMediaResource> MediaResourceLoader::requestResource(ResourceReque
cachingPolicy };
loaderOptions.sameOriginDataURLFlag = SameOriginDataURLFlag::Set;
loaderOptions.destination = m_destination;
+ if (!m_document->securityOrigin().isSameOriginAs(SecurityOrigin::create(request.url())) && m_crossOriginMode.isNull() && m_wasMainResourceLoaded)
+ loaderOptions.loadedFromOpaqueSource = LoadedFromOpaqueSource::Yes;
+ else if (!m_wasMainResourceLoaded)
+ m_wasMainResourceLoaded = true;
auto cachedRequest = createPotentialAccessControlRequest(WTFMove(request), WTFMove(loaderOptions), *m_document, m_crossOriginMode);
if (RefPtr element = m_element.get())
cachedRequest.setInitiator(*element);
diff --git a/Source/WebCore/loader/MediaResourceLoader.h b/Source/WebCore/loader/MediaResourceLoader.h
index e8c7e08f80ef..56697282020b 100644
--- a/Source/WebCore/loader/MediaResourceLoader.h
+++ b/Source/WebCore/loader/MediaResourceLoader.h
@@ -67,6 +67,7 @@ public:
private:
void contextDestroyed() override;
+ bool m_wasMainResourceLoaded { false };
WeakPtr<Document, WeakPtrImplWithEventTargetData> m_document WTF_GUARDED_BY_CAPABILITY(mainThread);
WeakPtr<Element, WeakPtrImplWithEventTargetData> m_element WTF_GUARDED_BY_CAPABILITY(mainThread);
String m_crossOriginMode WTF_GUARDED_BY_CAPABILITY(mainThread);
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: Big Bear
Key insight. A classic wrong-variable off-by-one: the escape end-of-input guard compared the global cursor m_index instead of the in-regex cursor regexPosition, so a pattern ending in a lone backslash slipped past validation and let the tokenizer index beyond the input buffer.
Patch Details
diff --git a/LayoutTests/fast/url/urlpattern-invalid-pattern.html b/LayoutTests/fast/url/urlpattern-invalid-pattern.html
index f81cda2fc7b7..9b6c5aff56f8 100644
--- a/LayoutTests/fast/url/urlpattern-invalid-pattern.html
+++ b/LayoutTests/fast/url/urlpattern-invalid-pattern.html
@@ -5,5 +5,6 @@
test(() => {
assert_throws_js(TypeError, () => { new URLPattern(new URL('https://example.org/%(')); } );
assert_throws_js(TypeError, () => { new URLPattern(new URL('https://example.org/%((')); } );
+ assert_throws_js(TypeError, () => { new URLPattern('(\\'); } );
}, `Test unclosed token`);
</script>
diff --git a/Source/WebCore/Modules/url-pattern/URLPatternTokenizer.cpp b/Source/WebCore/Modules/url-pattern/URLPatternTokenizer.cpp
index 71214612cf59..c8af6a4ef216 100644
--- a/Source/WebCore/Modules/url-pattern/URLPatternTokenizer.cpp
+++ b/Source/WebCore/Modules/url-pattern/URLPatternTokenizer.cpp
@@ -193,7 +193,7 @@ ExceptionOr<Vector<Token>> Tokenizer::tokenize()
}
if (m_codepoint == '\\') {
- if (m_index == m_input.length() - 1) {
+ if (regexPosition == m_input.length() - 1) {
maybeException = processTokenizingError(regexStart, m_index, "No character is provided after escape."_s);
hasError = true;
break;
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: an anonymous researcher
Key insight. op_instanceof was omitted from the inlined-getter OSR-exit return-location plumbing that its sibling checkpointed opcodes already had, so an OSR exit from an inlined Symbol.hasInstance/prototype getter had no valid resume point and hit an unreachable assert; the fix registers op_instanceof across all four layers and prunes the checkpoint handler to only the reachable state.
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: an anonymous researcher
Key insight. OMG loop OSR-entry didn’t match the IPInt tier’s scratch-buffer layout for exception slots, so values were read from the wrong indices and the rematerialized value stack was corrupted; positioning exception loads per the IPInt control stack fixes it.
Patch Details
diff --git a/JSTests/wasm/stress/omg-osr-stack-slot-positioning.js b/JSTests/wasm/stress/omg-osr-stack-slot-positioning.js
new file mode 100644
index 000000000000..c960b45e77de
--- /dev/null
+++ b/JSTests/wasm/stress/omg-osr-stack-slot-positioning.js
@@ -0,0 +1,118 @@
+//@ skip if !$isWasmPlatform or $buildType == "debug"
+
+function instantiate(moduleBase64, importObject) {
+ let bytes = Uint8Array.fromBase64(moduleBase64);
+ return WebAssembly.instantiate(bytes, importObject);
+ }
+ const report = $.agent.report;
+ const isJIT = callerIsBBQOrOMGCompiled;
+const extra = {isJIT};
+(async function () {
+let memory0 = new WebAssembly.Memory({initial: 2657, shared: true, maximum: 5930});
+/**
+@param {I64} a0
+@param {FuncRef} a1
+@param {FuncRef} a2
+@returns {[I64, FuncRef, FuncRef]}
+ */
+let fn0 = function (a0, a1, a2) {
+a0?.toString(); a1?.toString(); a2?.toString();
+return [230n, a1, a2];
+};
+/**
+@returns {void}
+ */
+let fn1 = function () {
+};
+/**
+@returns {void}
+ */
+let fn2 = function () {
+};
+/**
+@param {I64} a0
+@param {FuncRef} a1
+@param {FuncRef} a2
+@returns {[I64, FuncRef, FuncRef]}
+ */
+let fn3 = function (a0, a1, a2) {
+a0?.toString(); a1?.toString(); a2?.toString();
+return [1376n, a1, a1];
+};
+/**
+@param {I64} a0
+@param {FuncRef} a1
+@param {FuncRef} a2
+@returns {void}
+ */
+let fn4 = function (a0, a1, a2) {
+a0?.toString(); a1?.toString(); a2?.toString();
+};
+let tag2 = new WebAssembly.Tag({parameters: ['i64', 'anyfunc', 'anyfunc']});
+let tag4 = new WebAssembly.Tag({parameters: []});
+let global0 = new WebAssembly.Global({value: 'i64', mutable: true}, 2544446354n);
+let table0 = new WebAssembly.Table({initial: 90, element: 'externref'});
+let table2 = new WebAssembly.Table({initial: 80, element: 'anyfunc', maximum: 80});
+let table4 = new WebAssembly.Table({initial: 21, element: 'externref', maximum: 557});
+let table6 = new WebAssembly.Table({initial: 12, element: 'externref', maximum: 541});
+let m1 = {fn0, fn1, memory0, table5: table2, table6, table7: table4, table8: table4, tag3: tag2, tag5: tag2};
+let m0 = {fn2, fn4, table4, tag2};
+let m2 = {fn3, global0, table0, table1: table0, table2, table3: table0, tag4};
+let importObject0 = /** @type {Imports2} */ ({extra, m0, m1, m2});
+let i0 = await instantiate('AGFzbQEAAAABLwpgAAF/YAABcGAAAGAAAGAAAn5wYAAAYAAAYAN+cHAAYAN+cHADfnBwYAN+cHAAApACFQJtMQdtZW1vcnkwAgPhFKouAm0xA2ZuMAAIAm0xA2ZuMQACAm0wA2ZuMgAGAm0yA2ZuMwAIAm0wA2ZuNAAHBWV4dHJhBWlzSklUAAACbTAEdGFnMgQACQJtMQR0YWczBAAJAm0yBHRhZzQEAAICbTEEdGFnNQQACQJtMgdnbG9iYWwwA34BAm0yBnRhYmxlMAFvAFoCbTIGdGFibGUxAW8AHAJtMgZ0YWJsZTIBcAFQUAJtMgZ0YWJsZTMBbwAeAm0wBnRhYmxlNAFvARWtBAJtMQZ0YWJsZTUBcAE1+AMCbTEGdGFibGU2AW8BDJ0EAm0xBnRhYmxlNwFvAQGdBgJtMQZ0YWJsZTgBbwEOqQYDBAMABAkEHwhvAFRvACdvAQfDBW8BVogDcABGbwBJcAFdoQNvAE0NCQQABwACAAkABwZeDnwBRNDElsQj4bn5C28B0G8LfQFDAbNWrQt/AUEDC3AB0gcLcAHSBAt/AUECC30BQ5odw5MLfAFE2R6H/Xip+EsLcAHSBAt/AUE/C28B0G8LcAHSAwt/AUGegKIDCwf7ARoIZ2xvYmFsMTADCghnbG9iYWwxMwMNB2dsb2JhbDgDCAhnbG9iYWwxNAMOA2ZuNQAGCGdsb2JhbDExAwsHZ2xvYmFsNQMFCGdsb2JhbDEyAwwHZ2xvYmFsOQMJB2dsb2JhbDIDAgdnbG9iYWw0AwQHdGFibGUxMwEOB2dsb2JhbDMDAwR0YWcwBAAHdGFibGUxMgENB3RhYmxlMTEBDAR0YWcxBAMHZ2xvYmFsNgMGA2ZuNgAHB3RhYmxlMTABCwdnbG9iYWwxAwEHbWVtb3J5MQIAB2dsb2JhbDcDBwd0YWJsZTE1ARAGdGFibGU5AQoHdGFibGUxNAEPCcYGCgVwTdIFC9IFC9IBC9IFC9IHC9IGC9IBC9IGC9IEC9IIC9IEC9IIC9IDC9IHC9ICC9IDC9IAC9IAC9IHC9IIC9IFC9IDC9ICC9IBC9IAC9ICC9IEC9ICC9IHC9IHC9ICC9IDC9IHC9IDC9IGC9IHC9IAC9IFC9IFC9IHC9IHC9IBC9IEC9IIC9IEC9IIC9IHC9IIC9IDC9IHC9IHC9IAC9IBC9IAC9IHC9IHC9IBC9IDC9IBC9IHC9IIC9IHC9IDC9IAC9IHC9IIC9IAC9IGC9IIC9ICC9IEC9IIC9IBC9IEC9IFC9IBC9IHCwdwKdIEC9IGC9IBC9IBC9ICC9IHC9IAC9IAC9IAC9IGC9IGC9IAC9ICC9IEC9IIC9IBC9IFC9IIC9IAC9IFC9IHC9IHC9IBC9IHC9IDC9IHC9IFC9IDC9IHC9IGC9ICC9IDC9IBC9IFC9IAC9IHC9IGC9IFC9IDC9IIC9IDCwMAFgQAAwQFBgMBBQQDAQYEBgYFCAUBAwcDAF4DBQUGAwMIBwAAAwIGAgUAAwIDBAEGAgUGBwIFAQACAgcDBQEHAwADBQIABAgEBgIEBQcBAgEBAwgHAgICAwUFBAQAAQIBCAQEBQcEBAIFCAgHAgcDAwUHAwEDBwcGBXBj0gEL0gQL0gML0gcL0gAL0ggL0gQL0gQL0gQL0gUL0gEL0gML0gAL0gQL0gML0ggL0gYL0gUL0gML0gUL0gAL0gYL0gML0gIL0gcL0gEL0gUL0gEL0gML0gIL0gEL0gcL0ggL0gcL0gcL0ggL0gML0gEL0gAL0gcL0gYL0gML0gIL0ggL0ggL0gcL0gML0gAL0gML0gAL0gcL0ggL0gcL0gcL0ggL0gcL0gUL0gQL0gAL0ggL0gYL0ggL0gYL0gAL0gUL0gQL0gEL0gYL0gIL0gEL0gEL0gAL0gAL0gIL0gIL0gYL0gUL0gcL0gML0gQL0ggL0gEL0gEL0gUL0gIL0gAL0gcL0ggL0gIL0gYL0gQL0gYL0gQL0gUL0gEL0gcL0gML0gcL0gYLAgVBGAsAAgADBgVBHgtwAtIBC9ICCwYCQQALcALSBAvSCAsCBUEZCwACBQYGD0HaAAtwAdIHCwwBBQrLTgP/EgYCfgB8A38BfgF9AXwgALokCSAAeiMKIw0QCD8A/BAFcCUF/AkDBn5CrQEkAAZ8QRkRAAUkB0E1JAsDABAH/BAFQQJwBAIGfxAFRQRADAMLIwlCrAECQAYEBgUgAAZ9A34gAkEBaiICQQJJBEAMAQsQByAABm8QAQwHC0TBXKCRobBK2SAADAkACwwIC/wBDwsMAQtC3Kqq1c6w1fZ10gIgAQwFC9IIBnwMAgELDAMLGkEADgEAAAsCQAIFIwcOAgEAAQsGASABQdK8DUEBcA4BBAQBGRABIwkMAwskBgtCANIHBn/SAERnJMmm6K4ZT0KW2yTDJADSByMHA3wgAQwEAAtE0mYbYcjjxKkMAgsMAwALPwAPCyAAPwANAMQiAAwACyQAQeOlA0MRqidBA3wgAUQc7Cz7udVQztIF0gNDSbyhHIsgASMLDAEAC5sCfQYDBkAYAAwACwJ9AwECAwwAAAtCEyQAQfUADwALAAsAC9IAIAEjAtIBQuIABnwDAQYGGAIQAgJA/BAADgEAAAsjAUPbptWlIAH8EBAMAgALRM1sJew2PYROPwANAAwACyAAQtCh6IPJ1AAhAUO2ASRF/AUGfdIA0ggjAwZwPwD8EA0NAkECcAQAIADDQidDXlObMvwEBkAGfwwBC0EBcA4BAAALUCQEAkAMAAALBm8CAUNfwKKjDAQAC9IEIw4kC/wQEEECcAQEIwtB8/GnMk1B4zgMBQAFIwUMAwALQSRBLkEo/AwEAtIC/BAKDAQL/BABPwCsIAEgACIAJAAhACEBQf//A3EgASAA/kwAACIB0gLSByML/BAOQQJwBH7SByAAAn4Cf0EeEQYFQQtBOEER/AwABQIDAnAgAAwEAAsACwALAAsABSAADAAACz8AJA4CfQYGAwJBHhEDBQJvIwH8B8IkAENKCebOIABCm7bnRCQAJAAMBgALAAtEZVwlYh4by38jALn8BwJ/EAEGBgwCCwwBAAsCfAwBAAufvSIAvyQBDwtBGhEABUQnnyKWssFCPCQBJAtDMZR05QwAAAsMAgAFEAJDRCaYJfwQAQwDAAv8EA1BAXAOAQICAAv8EBD8EAUPCyABIwkkAbUkCPwBIwL8EAdBAnAEfiAA/BAJJA4kAAMAIwZD/cqMTSMNBnAQB0SW5pcGxyYqhgJwAwECAiAHRAAAAAAAAPA/oCIHRAAAAAAAADJAYwRADAULDAAACyABDAQACwAL/BAHDwskBj8ADAIACwwBBQIDDAAAC9IEQuPOvLzlyp/8ZgZ+IAAkAEEfEQYFBgQgAQwCCyQGDAALIgAMAAsiACQAIwRBAnAEAz8AQu0BPwBBAXAOAQAAC9ICQ8OesDI/AEECcAR+BgMCAgYAIAAgAQwDCw0A0gEGfQwCC5FBhwz8EAJBAXAOAAML0gEgASAADAELBgP8EAf8EAFBAnAEBQUGfETz/6jRp5RBmSMFQ66d09dBkBdBI0EQQSX8DAAPDQL8EA4GfAZwRP+o4yIXE2bwQdfhqbwDDQMGfwwEC0ECcARwDAQABQZwAgUMBgALQwMcqFokCAYFGSMD0gZBvs29B/wQAUEDcA4KAAYHBwYABgAHBgYL/BAPQQJwBH8GBBABDAgLDAIABUI/DAgAC0ECcA4CBQYFCwJ8DAUAC0RcFdRIfIDMxZ1mDAcLBkAMBAvSBgN/BgYGBQMBAwRDFKtF8CQIBgZEM3kGTHKhkuHSB/wQCA0AQjEMCwsCQAwJAAsACwALQn9C1LDk756AwrwziCABPwBBAXAOAQgICwwFCyAGQwAAgD+SIgZDAAAQQl0NAAYDBgAjAQwEC0H7AEEDcA4DBgUAAAsMBQALQQJwDgcDBAQDAwMEAwsgAUKs18+d36B0Bm8jDgwGBwMjDUQj87ETSmDI7QwBAAsjAkPkHLN/JAj8EAIPCwwACyMODwsPCxACBgACfBACQR4RAgVBHxEDBQYGAkDSAyABA30MAQALAAsGcAwBAAvSBNIAIABC2wEDcBAFRQRADAELBgMMAgvSBiMEDwALBnBCpNndgusCA3AMAgALROrPTR2nftsCDAILJAYGQAwAAQv8EANBAXAOAQAACxABAnwCQPwQCQwDAAsACwwAAAtEj8x5hWOkk8v8Aw8LQQJwBAUFBgEjDPwQBEEBcA4BAQEAAQALQTBBEkEB/AwAAgNwIAVCAXwiBUIaVARADAELRAxLXwFWwMugBn4CBSAFQgF8IgVCHVQNAhAFRQRADAMLDAMACwIDQusADAEACyAAIAAMAwsjBSABtEH9oQ4GbwIFBn8gAEMAAACAjkO5wpx/AkAMAgALu5lBhpWKxwUMAAsgAAwECwwCAQvSBEHBACQLQvcAukQAAAAAAAAAgPwQDkEBcA4BAQEL0Q8BC9ID0gEgAAZ+Q16Q+TKR/BAADAILDAABAAUGBPwQASQL0gLSBAJ9AnxBIUECcARADAAABdIHIwRBAXAOAQAACyMDDAEACwALIAHCDAEHBAYHEAgCQAwAAAv8EAREtVX5rIg4+X9BLEEWQQ38DAQNIwwkDAkBCwIAAgVEvhHC/QdiN4sjBiQKJAkGACMEDAALQQFwDgAAAQELIw5BAnAEAkKRAfwQB0EBIwdBAXAOAQAAAQsCfdIA0ggjAiQM/BAGA33SAkNFLN6b0gYgAdICIAG1DAEACwALJAM/AA8AC0QT+335BABiLkEqQRxBB/wMAAUGbwZwBgIMAAsCAgwAAAsCBQwAAAsjASMEBAMGBQcDAgcGB9EMCAELDAIAC0LlAQwFCyAADAQBC5tCJCQAQbjUHwwECyMHDwvSB9IDIABENpI9efcTz7f8EAsPCz8ARC56pS/tgPf/Am8CbwMD/BAPJAsGBgICDAAAC0HeACQOQuPpfkJ5RPU7AM2WJxPLQzzcLlxBMkEEQQz8DAQPIwQPAQsgBkMAAIA/kiIGQwAAAEJdDQADBiMLQsEBuj8ADwALAAsACwAL/BAK0gNDJkX3iiQDIAF6AnxEqfrJ1wUjm+tEiKcQePTVcjMMAAAL/AckAAwACyQABnBBiJcBQyndNei8QQFwDgEBARkCf0LUg/u6qoUBJABByvuiCQJvIwUMAgALAAsPCyQFJAggAANAIAVCAXwiBUIfVA0AIAJBAWoiAkEaSQ0AAgQCBgwAAAsGfUPfZSugDAABAAtB0wBBLUEG/AwAD/wBDwALAAsGf/wQCNIAQRtBBUEk/AwADSMEDAAL/BANDwvpMAQDfwF+AX0BfEGy2ezHAUH//wNxMgD6ASMEBn9BMyQOAgUMAAALQ7BqfR8kA0RjPQHTEQ0tS0PH919jQhkkACMKIwQMAAsjDSQGQQJwBAYMAAAFBgQCBAYBDAMLJAoDcAN8EAJBHhEFBQYEEAEMBQsMBQALAAsACyMBtiQIGAEkCiMKDAEBAAALQdUB/BAPcCUPIwkGfyMHQQJwBHBDHUdDnyQI0gUjCJEGfUMcWnM8/AEjDgwCC9IGQ/Rn1X9BoAFBAnAEb0HOAEECcAR+QZ0CQQJwBAQQAQYFIwQMBQEACxACPwCzBm8GBQJv/BADBH4jCw0CAgIQAQwDAAsABRAB/BANDAgACwwEAAskAgwACyMAIw4MBQtB67756gNBAXAOAQICAAVBHhEGBRAFQR5B0gBBDvwMBAJBAnAEb0Hc0+o//BAFcQwFAAUCAwYFIwYCfgZ/AgXSAUENDAEACwwCC0KUi1AkAAwIAAsjB0ECcAQCDAEABQIAEAUMCQALAAskAAwGCwYGAgIDABACDAEACwALGAcMAAALQQhBxgBBBPwMAAUGAUHtAAwGCwwEAAsMAgALQfMBDAMF/BAFBHAjCgwDAAUjA9IDRBxY8xHYgyOrAkAGBQwBC9IH0gNCy3FBLwwFC5ydJAk/AAwEAAsMAgAL0gbSBiML0gcjAMO60ghDJkQRzEPafK/UIwhBBrdEB8PWTZfpkgmbQdT+A0GJnpG7BUH30AH8CwAGfgYGDAABCxACIwX8EA9BAXAOAQICCyMAhyMMQraai7zIACMCDAAFAgYMAAALEAIGAgwACwIFDAAACyMMQdQB0gEjDgwCAAskDCMNIwPSCEHGveD+AQwBAQVDDPEtBtIIIwj8BAJ9Qg1EAAAAAAAA8P8kASMBJAnSA0KTgMuv9bbLAQZ/AgQCBvwQDAwFAAsACw8LIwH8AkEBcA4BAgIL0gcjDCMEDAEACyMF0gXSAUKDgLjV9AgCbxAFJAsGAAMCQ7eBgXVEn89yc/9ad8dCzgEjASQBJACcJAEkAwIBA28QBUUNACAAQQFqIgBBJkkEQAwDC/wQCAwDAAsACwALIwL8EAsNAQwBC0Sq2odKabjxf/wQCwwBAQtEWmeVGMPsXtUDfQJ9IABBAWoiAEESSQ0BAgUgAkEBaiICQQRJDQIMAAALIwIkDCABQQFqIgFBH0kEQAwCC0ERQc0AQRP8DAQFIARDAACAP5IiBEMAADBBXQ0BIAFBAWoiAUExSQ0BIw4kBCAEQwAAgD+SIgRDAAAAAF0EQAwCCyAFRAAAAAAAAPA/oCIFRAAAAAAAAERAYwRADAILIwD8EAsjBiMBJAEjCUELQSlBG/wMAAXSAUQ9p7zhcOltgwN+IwMMAQALAAu7Q8IFfdb8EAkCbyAAQQFqIgBBG0kNASMMDAAACyQC/BAEcCMCJgRBnMo7QtkBQ69GxaAjBEECcAQDQRoRAAVpQQFwDgEAAAv8BVVuBn4CBAYGEAVFDQMMAAEACwMFIwxDAACAfwZ9Iw4Gb/wQAQwGCz8ADAUAC9ID0gD8EAMCfiADQgF8IgNCElQNAUNuPNd/PwBCncPqkHtQIwsMBQALAAsACwwDCz8A/BABQQFwDgEBAQsCb0Tqcfte6v6Y8gJvAgYMAAALBgAGACMC0gIjBQZ9BgUGBUHR0oaneUECcA4IAAABAAAAAQEBC0EfEQIFDAALBgUCBQwBAAsMAAsjDvwQD3AlDyQNIwgMAAv8BEOOpC2JQiH8EA8MBAskB0ElBn0CfgJ8Q2ywlSfSAtIA0ggjBj8AJAQCfCMMDAUACwALAAvCJAAGBERsf4HRybCi1SQBIwYGQEEfEQYFDAALIwQMBQELJAYkAAYFC0P8m4pnGAMjACQAQTVBxwBBDfwMBA1B9LLwiHgjAL8kCQwACwwCCwwAAAsGcCMCA0AGACMAIwCIQtsBIw4MAwskDvwQCrOM/BAAQRhBxwBBAvwMBA8kB478AQwCAAv8EAskB0KNdNICQ6YfNG0CbyMORQwCAAv8CQL8EAP8EANBAXAOAQEBAAtBy/jW/QAMAAtBAnAEfAYBBgMMAAsGfyMHDAALQ6y2pEsGQAN9BgMjDdIDQ+RYUVgkA0ICJABDCR2LwZECbwYEQ/pLRDIkCAICDAUAC0SMDnUXE2ACQ0LtACMBDAYL/BAODgUGBgYGBgYLJAICQESLcZd3RNogi/4DAAwFAAuLAnAMAwALIwtBAnAOAgIAAgEAAQsMAQALQx1jlvUGb0ToIwuliuzjaNIDPwCzvEEBcA4BAQELQgc/AEEBcA4BAAAL/BAG/BADcCUDIwojASQBDAALJA0GARAC/BANJAf8EBAEAAYD0ggDQCAAQQFqIgBBAUkNACABQQFqIgFBFUkEQAwBCxAFRLgVF7JX60+TBm8DQAJ+QZ/w0gVBLEHBAEEE/AwAAgwFAAsACwYEIwwjAAZ/BgUMAAsDASADQgF8IgNCHVQNBAYDQxQPqAaQQuKR583XQdII0gbSBUEiQcgAQQT8DAAP0gICcAIGAgIQAgIFDAIACwALAAsjByQHEAVFBEAMAwtCKbT8BPwQDgwDAAvSAkO09eCw/BAGDQAkA0PMr9pkIwI/AAR8DAEABQIAIABBAWoiAEESSQ0HIwJBKgwAAAskBCAAQQFqIgBBJkkNBgMAEAVFBEAMAQtBpcjZqAMMBAALAAvSBSMKJAX8EA4NBiMCDAQLIAFBAWoiAUEcSQRADAUL0gDSCEPC9+V9jY8CfURewrHIl8JkCvwQCEEBcA4ICQkJCQkJCQkJC/wQDAwBAAsjBgwGC0EBcA4BAwMLIwg/ACQE/AX8EA4Gf9IIBn8GA0L31qJ/IwcEAAYB0gMjBgwJCyQFDAEABQYCBgUMCAtDkwZTnUObgjwsJAjSBvwQCA0APwBBA3AOAgAHAgtCtuvN1o+d460BJADSCCMA0gNEAAAAAAAAAAAMCQALDAIBAQsjCwwACwwACwwDC0I5An8MAgALAAsjDiMEQQJwBG8GAgYFAm8Gb0HPmQFBA3AOAwUDAgULIwcDfAwFAAsACyMOQQNwDgMAAwEDCwYGIwgjBSMB/BAIDQAMBgsMAhkDAAwBAAtBAnAOCgICAAIAAAICAgAACwJ/DAIACwAFEAEGBgwACwYAAwMjC0ECcARAAgECBQwCAAsACwAFIAFBAWoiAUExSQRADAILEAVFBEAMAgsMAAALEAJEo3KmR0wBewgMBgALDAILDAIACwNvQ93ap1XSAyMIBn0GQAsQBUG124nVfQ4BAwML/BAQDQEGcAwCCyQGiyMOQ7h6wylD7O2O1wJ9AwUgAkEBaiICQSJJBEAMAwsMAwALAAsACyQMQufx15F7wwZ8DAELIwJDO9Vm2wZ+AgUMAgAL0gD8EAUaQQkOAQEBAQsGfQYFA0BETPcz90+ASTkMBgALQt4AxKcMAwsGfQwCC7wMAgv8EAkkByMBIwxB9bkCQQVBAfwIAQAkDAwDAQtC9QBEyESUI4TRg+8CfwZ9Iwn8EATSBAJA/BAKDQDSByMODQDSB0GrAUTcZslDP7RTaAwFC/wQAEECcAQA/BABDAMABSMJ0gTSAQZ/Bn8/AEECcAQAEAJBleq5onoFBgJEFPg6AmUqLGIMCQsGfQIDRArf2dqGysMxJAkMAAAL0gEjBAwGAQsGfUEiQRlBDfwMAAIGBgcFAgRBuvccDAUACwwJAAsjBtIHQ4Q/8r0MAAskA0Hn0Z/XAwwDAAvSByMHDAUAC/wQCgwACwwACyMF0UECcA4CAQIBCz8AIwtBAnAEBQwAAAUjCwwCAAs/ANIFQ2LdNh9Bqd6wDgwBCwwABQJ/RMGHAsOGF/H/QvuGrfHl3dM9wqdBAnAEQAJwDAEACwAFDAAACyQBRK8ATulIOIX8A34GAAZABwIQBUUNAvwQDQ0A0gADcBAF/BAHPwAMBQALBnAMAQsjDvwQAwwDAAsGACMBDAYLQbQBDQMEfxAFRQ0CAgBBqvvGDwwBAAsABSMA0gAjCPwFIw4MAAALrNID/gMAQ29AW64kAyMIQmf8EAX8EAgMAwsMAQAL/BALDAELJAsCAUKnr9K0CnmnDAEACz8ADAALIwkjCCQDAnAjBfwQDkECcA4CAAEBCyMB/AIkCwJ90gcjACQAAn0jByMDIwRBAnAOAQABCwJ+IwQkCz8AAnBDWFO3vfwQCgQE0gHSBdIGQcEAQRdBA/wMBAICcCMKDAUACwAFBgMMAAsjAwwDAAskCiMG0gDSAiMFDAMACwAL0gL8EAoGQAwAAAsjAEQhEp2L4+5gxiMHDgECAgskAz8AQQFwDgEAAAskBdIDRMLnx8JPILMVDAAFQR8RAwUjBEECcARADAAABQJ9QeoMQQFwDgEBAQsjBtIBIwkGfgwBCyMODgEAAAsGbwNwAgEGBQMABgYYAUOQ9rd/Iwb8EA0jDkEBcA4BAQELDQAgA0IBfCIDQgVUDQIQBSMHQQFwDgEAAAtBwoHjpXg/AEECcAR+QR4RAgUGAwwAC0Gg7QNBEQZ/AwAQASMIQe8cQQJwBEDSBfwQAgwCAAUGBdIF0gHSAyMADAQBCwYCDAALDAAACwZ9QYMBQQJwBAMgBEMAAIA/kiIEQwAANEJdDQYgAEEBaiIAQQlJBEAMBwsgAUEBaiIBQSdJDQIQBUUEQAwHCwZ/AgQgA0IBfCIDQi9UBEAMCQsQBQJADAAACwwFAAsMBgu+JAgGfBAFRQ0DBgYYCgZ/IAFBAWoiAUEFSQ0I0gX8EA0MAAtBAnAEAUSxqBI9Mdmg1wZvEAVFDQkGBhlE2S+Tpr3OQSdCpgGnQQJwDgIABAQBCyADQgF8IgNCFFQNBRAC/BANQQFwDgQDAwMDAwv8EBANCQwJAAUgAUEBaiIBQRBJDQQMAgAL/BAHQQFwDgEBAQtCoQH8EAtBAXAOAQAAAAALAgUGBQwACxAFQQFwDgEAAAtBHxEDBSACQQFqIgJBEEkNBQYAAgMMAAALIwYMBQtBAnAEcAYDBgILAwAQBUUNACAFRAAAAAAAAPA/oCIFRAAAAAAAADJAYwRADAULIwQMBQALDQAGBAwBCwJvRKPg/+eJQLRQDAoACyMFAn4CBiMMIwsNCgwKAAsACyMGDAELIABBAWoiAEEbSQRADAcLAgYgBUQAAAAAAADwP6AiBUQAAAAAAAAcQGMNAxAFRQRADAgLQquB65e6fiQAIwcEbyACQQFqIgJBCUkNBCABQQFqIgFBEUkNCCAFRAAAAAAAAPA/oCIFRAAAAAAAAAAAYwRADAkLQR4RAwUgBUQAAAAAAADwP6AiBUQAAAAAAAA3QGMNCCAAQQFqIgBBAkkEQAwFCyACQQFqIgJBEkkEQAwFCyAEQwAAgD+SIgRDAAA8Ql0EQAwFCyABQQFqIgFBAUkNCCAAQQFqIgBBLEkEQAwFCyMFDAIABSMMDAkACwJvBm8gAUEBaiIBQQ5JBEAMBgsgAEEBaiIAQRZJBEAMBgsgBEMAAIA/kiIEQwAAgEFdDQkCBgMB0gRDAACA/7sMDQALAAsjBQwDC0M986qzDAMACwAL0gQjAgwHAQUgBUQAAAAAAADwP6AiBUQAAAAAAABHQGMEQAwDC0K3y8WZ482Tv30CfiAAQQFqIgBBHkkNAyAAQQFqIgBBIkkEQAwIC/wQAgwEAAsAC0Qr7uc2jK4PIAwHC2AMAQALDAALJA4GQAYDDAEBC0O4BhD7IwRBAnAEfwwBAAUCfgwCAAsAC0ECcARwIAVEAAAAAAAA8D+gIgVEAAAAAACAQUBjDQQCfhAFQQFwDgECAgu6JAkMAQAFDAEACz8AQQFwDgECAgtvJAdC+VQMAAUjAEOb8cmePwBBEkHVAEEJ/AwEAiQL0gcjB7cMBAALJAD8EA9wJQ8MAAsjDCQC0QRvQY/hACQOBgUGBQYBDAELQ+XxepMkCCQFC0EaEQAFQQFwDgEAAAsCBSMF/BAIQQFwDgEAAAsgA0IBfCIDQh1UDQECBSAEQwAAgD+SIgRDAACQQV0NAkNNLoKP/AD8EAUaQQAOAQAACyACQQFqIgJBJEkEQAwCCyAEQwAAgD+SIgRDAABEQl0NAUEBJQgkAgJvQabOBUSmdtfrfnhGgwN+IABBAWoiAEERSQ0ABgMLIARDAACAP5IiBEMAACBCXQ0AIAJBAWoiAkEtSQ0DIwUkBUL2ACQAIAVEAAAAAAAA8D+gIgVEAAAAAAAALkBjDQMgBUQAAAAAAADwP6AiBUQAAAAAAABGQGMNAEQpAxZMcoEduAwFAAsACwAFIAVEAAAAAAAA8D+gIgVEAAAAAAAAQ0BjBEAMAgsgAUEBaiIBQStJDQECAQMG0gUaCyMFBkAgAUEBaiIBQShJBEAMBAsgBEMAAIA/kiIEQwAA4EBdDQP8EAVBAXAOAQAACwwAAAsCQAJABgEMAgAL0UECcA4CAQAACyMEQQFwDgEAAAsCf0LyncfbAUG6ASQLQcsAQR1BDfwMAA96GkGZgOn7AAwAAAskCyQGIAJBAWoiAkEmSQ0BBgBE4gzym8jXXyAMBAskDhAFJAcgAEEBaiIAQSVJDQFCnwFDwLTyehoGfCMJCyQJJAAGfSMAJABBp8KYAyQEBgMgA0IBfCIDQgpUDQNCtAEaAkALAgBEUBWyNJefB5YMBgALQQFwDgEAAAALIABBAWoiAEEBSQ0CAwRBnNUDQaYBQaog/AsAIAJBAWoiAkEvSQRADAELBm8DAwIEIAVEAAAAAAAA8D+gIgVEAAAAAAAA8D9jDQFC5pnWatIFQQkjDiQHQeUAJAskCxpBMyUFDwALAAsjDAwDCwwCAAsPCyQDBnxDGtTp9wZwQTIlDyQFQSklAvwQBA0AGAAaJAg/ABogA0IBfCIDQgNUDQLSCBojDCQCIwkkAUNaAsP/Bm8CfUMR+rh//BAOQQFwDgEAAAuNIwYjCCMGGkHHmQJB2QBBgtUA/AsAJAhB6Z73xgNBAnAEfPwQBSQLAgUCBQsCBQZv/AkEIARDAACAP5IiBEMAAJBBXQ0HDAILDAMACwtENHRM5V4wHV1EsQwfSwOo0f9kQQJwBAFBKCUCQSpBBUEY/AwADQwAAAUDBBABQsoBQQdBB0E//AwAD3oGcEEzJQ8jBEECcA4FAgIAAgIACwskBkNNJTFWJAj8EBBBHUHUAEEM/AwEDSQO/BANPwAaJAcaBgUgBUQAAAAAAADwP6AiBUQAAAAAAIBGQGMEQAwHCwZ9QQoOAAELGkRwV0bUyh1aSyMBDAQAC0E1JQ8L/BADGhojAQwCAAVEj7W4VBXJGc4jDSMDJAMkBQwCAAtChgEjASMO/BALJAtBAnAOAgUBBQAAAQsMAQELtkPjdNT/QfoBJAv8BUHUxAJB+8wBQcbZA/wKAAAkACMBDAMLDAEACyQGEAECBkG/9QlBAXAOAQAAC0T5eiyQdo18qtIHIwgkCCMEQf//A3EyAf8HPwAkDiQAQg4kANIF0ggaPwAkBBrSCBojCCMJnANwIwv8EANB/v8Dcf4TAQBJBG8DASMFC0EEQTpBBPwMAAUkCgYBBgBBBgwAC/wQACQOQQJwBAFCggEkAAYBEAVFBEAMBQsjCgskDSAAQQFqIgBBAUkNAwICC0EKJQUMAAAFQQ0lDQwAAAsLJAogAEEBaiIAQSVJBEAMAgsCAQIDA34gAkEBaiICQQZJBEAMBQsjAgwDAAsACwALAAUQBUUNASAFRAAAAAAAAPA/oCIFRAAAAAAAgEFAYwRADAILIABBAWoiAEEtSQRADAILBkAQBUUNAgJvEAVFBEAMBAsMAQALJAILAm8jAiQCIAVEAAAAAAAA8D+gIgVEAAAAAACAQkBjDQIjAiQMQRkRAAUkBwMBIANCAXwiA0IaVA0AQRYlBQsjAiQCQ+E6OJwDQCACQQFqIgJBHEkNAAskCCMEQQJwBAMQBUUNAwVByQBBBUEB/AwADwYDDAABAAcHJAb8EBBBAXAOAQcHCwwAAAskBUE4JRALDAIACyQC/BALJA4gA0IBfCIDQiFUDQAGASMF0ggGcAYDCwYCCwN8BgEjBgsMAQALDAQACwwACwvSCBokDdIFIw78EBBB2qLv+X1pBnxEzSbuiJ8H8/8LDAELJAxEamaYFgdq/McLIwdBAnAEAAYFDAALQQgMAAAFREbdrlnTLaMiIwkkCb1DZr5I8yQDGiMNIwQMAAALJA5BNUECQQP8DAQCnSQJn/wH/BAFJA4kAPwQB0GcAXgjAgZADAAL/BAAJAckDPwQEHAjDCYQJAX8EAJwIwUmAtIFGiQAIw0kCkL0ALQkA0OGGs+I/AQjBQwAC9wKDAFwAX0BcABwAXABfgJ/An8DfwF+AX0BfEIEIAUgCUECcAQBIwJEmJGEmzWbtE0CfgMBAgRDhxgXbz8ADQQCfQMBIA9CAXwiD0IdVA0AQay16wIEARAFRQRADAILAgAQBUUEQAwGCyMNIwkDf0GAw7LxAkHLAfwQCHAlCAZ/BgD+AwAGfBAFDAILIAsOAQsLCwwACwwBAAsACwJ/Q27O1JX8EApBAXAOAQMDAQtBAnAEbwYEIBFEAAAAAAAA8D+gIhFEAAAAAAAAEEBjBEAMBAsgDEEBaiIMQQlJBEAMBwsCAyANQQFqIg1BHUkNByARRAAAAAAAAPA/oCIRRAAAAAAAAD9AYwRADAgLIA5BAWoiDkEESQRADAULDAAAC0TJbQ/IrEVkoCQJBgIGASAMQQFqIgxBF0kNCCAH/BABIQsMCQsiBiAIQQJwDgIACgAL/BACAm8MCgALDAELIAEMBwVEn1B0/i0lCqzSBQZ8AgUgDEEBaiIMQRtJDQcCAUGekzwOAgELCwsGQCAQQwAAgD+SIhBDAAA4Ql0EQAwJCyABIAYMBAsMCQsQBUUEQAwHCwJ+IAxBAWoiDEEqSQRADAUL0gfSBwJwAwRE/koZmJ7aVwwCfSMKIwL8EAX8EAlwJQkGfwYDAgZC1gAMBgALDAALQR4RAwVEIcW643Za6fcMBQtBAnAEA9IB0gZC/gG6IAsNAAwFAQsgBCMN0gBCAfwQCQ4KAwsDAwMLAwsDCwMLBn8CQAwAAAtBrL2SAkECcARAIw4MAQAF0gQgBQwNAAsQBUUEQAwICxAFRQRADAILDA0LAn8gDkEBaiIOQQtJDQcgEEMAAIA/kiIQQwAAiEFdDQpE8d+tYXrb8j4kCQZ8IAEMAwtEAAAAAAAAAAAMBAALAAsGfA8BAQsMAgsMCQtBMkNEFPlVQQD8EAENCUH//wNxLACJBQ0EDAQAAQsGQBgIJAEGfD8AIAlBCyAAQs2A+6jw498WRPnwTBKrE6F8DAALJAkgACQA/BAMQQFwDgEICAtBzQENB/wQCUEBcA4BBwcABQwHAAsiBgwFAAsjCCEEDAQL0gjSB/wQDkECcAQADwAFDAUACyAFRCcgh/3L0IH2/AMaQQAOAQQECwwCAAsMAQsjBSMEQQFwDgABBSMGQvsBQtfjeCIHQt8Bp9IG0gVBw6bZlwMNASMM0gU/AA0BIwFDS9gus/wQAg0BPwANASQIJAH8EAQNAUEADgEBAQtBH0EyQQf8DAQFAggCCAYHDAIBCw8ACwYHDAEAAQsGBgwCCw8LIgUGCQJ/IwxBnN0CQZbk5sF9QaWZA/wLACMGIQX8EAtBAnAOAgECAgtBAnAOAgABAQELBgIGcEGOI0ECcA4CAQICC0MV8y76IAZEoi39JnDO6BogCEECcA4CAAEBC0LTAUNoINxNIw0jBgJ+RGvBjyYvu/7/JAEMAQALIwQNACEHQsvwvf+tpAYDfQwBAAsCfdIDQRhB//8Dcf4SAABBAXAOAQEBC5T8EAcNANIGRIj9ofYg7ZTsJAlEbRco/q9piPw/AA0A0gcgBCAE0ghCl+e10rWMzn4GQAwACyQA0gIgAEPti7lgJAN7JAAgBP4DACQIQfKUyL0DIgkGb/wQDj8ADQFBAXAOAQEBC9IDQugAIgDCup4kASAE0gAgAUT5VMQ36FP4/0QsuEEPaxAXLaTSBEML3pYCBn0jDCQMRArwgfDpsXm6JAEGfSMKJA0MAgsGfkIbv55CAfwQDkEBcA4BAAALIApBAnAEAQwCAAUPAAtDzP+Ch7xBAXAOAQEBC0H7wq7lfQ0AIwtBAXAOAQAACws4BQEGDqbuLUjIAQg0Vk5W1CsQPABB27MDCwexNdp88N+BAEHRLQsBdQBBqKsDCwncWuNpl/7i/7Y=', importObject0);
+let {fn5, fn6, global1, global2, global3, global4, global5, global6, global7, global8, global9, global10, global11, global12, global13, global14, memory1, table9, table10, table11, table12, table13, table14, table15, tag0, tag1} = /**
+ @type {{
+fn5: () => I32,
+fn6: () => [I64, FuncRef],
+global1: WebAssembly.Global,
+global2: WebAssembly.Global,
+global3: WebAssembly.Global,
+global4: WebAssembly.Global,
+global5: WebAssembly.Global,
+global6: WebAssembly.Global,
+global7: WebAssembly.Global,
+global8: WebAssembly.Global,
+global9: WebAssembly.Global,
+global10: WebAssembly.Global,
+global11: WebAssembly.Global,
+global12: WebAssembly.Global,
+global13: WebAssembly.Global,
+global14: WebAssembly.Global,
+memory1: WebAssembly.Memory,
+table9: WebAssembly.Table,
+table10: WebAssembly.Table,
+table11: WebAssembly.Table,
+table12: WebAssembly.Table,
+table13: WebAssembly.Table,
+table14: WebAssembly.Table,
+table15: WebAssembly.Table,
+tag0: WebAssembly.Tag,
+tag1: WebAssembly.Tag
+ }} */ (i0.instance.exports);
+table0.set(34, table10);
+table11.set(22, table4);
+table0.set(53, table0);
+table10.set(3, table10);
+table4.set(14, table9);
+table15.set(68, table10);
+global2.value = 'a';
+global10.value = null;
+
+try {
+ for (let k=0; k<16; k++) {
+ let zzz = fn6();
+ if (!(zzz instanceof Array)) { throw new Error('expected array but return value is '+zzz); }
+ if (zzz.length != 2) { throw new Error('expected array of length 2 but return value is '+zzz); }
+ let [r0, r1] = zzz;
+ r0?.toString(); r1?.toString();
+ }
+} catch (e) {
+ if (e instanceof WebAssembly.Exception) {
+ } else if (e instanceof TypeError) {
+ if (e.message === 'an exported wasm function cannot contain a v128 parameter or return value') {} else { throw e; }
+ } else if (e instanceof WebAssembly.RuntimeError || e instanceof RangeError) {} else { throw e; }
+}
+
+})().then(() => {
+}).catch(e => {
+})
diff --git a/Source/JavaScriptCore/wasm/WasmOMGIRGenerator.cpp b/Source/JavaScriptCore/wasm/WasmOMGIRGenerator.cpp
index 1f75b329a384..2e146c2b4901 100644
--- a/Source/JavaScriptCore/wasm/WasmOMGIRGenerator.cpp
+++ b/Source/JavaScriptCore/wasm/WasmOMGIRGenerator.cpp
@@ -4818,7 +4818,7 @@ void OMGIRGenerator::connectValuesAtEntrypoint(unsigned& indexInBuffer, Value* p
Value* load = loadFromScratchBuffer(indexInBuffer, pointer, value->type());
m_currentBlock->appendNew<VariableValue>(m_proc, Set, origin(), value.value(), load);
}
- if (exceptionVariable) {
+ if (!Options::useWasmIPInt() && exceptionVariable) {
Value* load = loadFromScratchBuffer(indexInBuffer, pointer, pointerType());
m_currentBlock->appendNew<VariableValue>(m_proc, Set, origin(), exceptionVariable, load);
}
@@ -4855,14 +4855,23 @@ auto OMGIRGenerator::addLoop(BlockSignature signature, Stack& enclosingStack, Co
for (auto& local : m_locals)
m_currentBlock->appendNew<VariableValue>(m_proc, Set, Origin(), local, loadFromScratchBuffer(indexInBuffer, pointer, local->type()));
+ if (Options::useWasmIPInt()) {
+ for (unsigned controlIndex = 0; controlIndex < m_parser->controlStack().size(); ++controlIndex) {
+ auto& data = m_parser->controlStack()[controlIndex].controlData;
+ if (ControlType::isAnyCatch(data)) {
+ auto* load = loadFromScratchBuffer(indexInBuffer, pointer, pointerType());
+ m_currentBlock->appendNew<VariableValue>(m_proc, Set, origin(), data.exception(), load);
+ } else if (ControlType::isTry(data))
+ ++indexInBuffer;
+ }
+ }
+
for (unsigned controlIndex = 0; controlIndex < m_parser->controlStack().size(); ++controlIndex) {
auto& data = m_parser->controlStack()[controlIndex].controlData;
auto& expressionStack = m_parser->controlStack()[controlIndex].enclosedExpressionStack;
ASSERT(&data != &block);
Variable* exceptionVariable = ControlType::isAnyCatch(data) ? data.exception() : nullptr;
connectValuesAtEntrypoint(indexInBuffer, pointer, expressionStack, exceptionVariable);
- if (ControlType::isTry(data))
- ++indexInBuffer;
}
connectValuesAtEntrypoint(indexInBuffer, pointer, enclosingStack, nullptr);
// The loop's stack can be read by the loop body, so the restored values should join using the loop-back phi nodes.
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: A website may be able to access sensor information without user consent
Researchers: Jaydev Ahire
Key insight. The authoritative, web-app-reconciled muted state must be computed before it is cached or broadcast; propagating the raw requested state to internals().mutedState and to content processes while only the later async message used the reconciled state left capture effectively unmuted, exposing sensors without consent.
Patch Details
diff --git a/Source/WebKit/UIProcess/WebPageProxy.cpp b/Source/WebKit/UIProcess/WebPageProxy.cpp
index c2857319785b..4a28da3e5e8e 100644
--- a/Source/WebKit/UIProcess/WebPageProxy.cpp
+++ b/Source/WebKit/UIProcess/WebPageProxy.cpp
@@ -9504,7 +9504,14 @@ void WebPageProxy::setMuted(WebCore::MediaProducerMutedStateFlags state, FromApp
if (!isAllowedToChangeMuteState())
state.add(WebCore::MediaProducer::MediaStreamCaptureIsMuted);
- internals().mutedState = state;
+#if ENABLE(MEDIA_STREAM)
+ auto newState = applyWebAppDesiredMutedKinds(state, m_mutedCaptureKindsDesiredByWebApp);
+#else
+ auto newState = state;
+#endif
+ WEBPAGEPROXY_RELEASE_LOG(Media, "setMuted, app state = %d, final state = %d", state.toRaw(), newState.toRaw());
+
+ internals().mutedState = newState;
if (!hasRunningProcess())
return completionHandler();
@@ -9516,16 +9523,9 @@ void WebPageProxy::setMuted(WebCore::MediaProducerMutedStateFlags state, FromApp
#endif // ENABLE(MEDIA_STREAM)
forEachWebContentProcess([&] (auto& process, auto pageID) {
- process.pageMutedStateChanged(pageID, state);
+ process.pageMutedStateChanged(pageID, newState);
});
-#if ENABLE(MEDIA_STREAM)
- auto newState = applyWebAppDesiredMutedKinds(state, m_mutedCaptureKindsDesiredByWebApp);
-#else
- auto newState = state;
-#endif
- WEBPAGEPROXY_RELEASE_LOG(Media, "setMuted, app state = %d, final state = %d", state.toRaw(), newState.toRaw());
-
auto aggregator = CallbackAggregator::create(WTFMove(completionHandler));
forEachWebContentProcess([&](auto& webProcess, auto pageID) {
webProcess.sendWithAsyncReply(Messages::WebPage::SetMuted(newState), [aggregator] { }, pageID);
diff --git a/Tools/TestWebKitAPI/Tests/WebKit/GetUserMedia.mm b/Tools/TestWebKitAPI/Tests/WebKit/GetUserMedia.mm
index 961f574766e2..8f258d6448ea 100644
--- a/Tools/TestWebKitAPI/Tests/WebKit/GetUserMedia.mm
+++ b/Tools/TestWebKitAPI/Tests/WebKit/GetUserMedia.mm
@@ -2054,6 +2054,62 @@ TEST(WebKit2, getUserMediaWithDeviceChangeWebPage)
done = false;
}
+TEST(WebKit2, GetUserMediaAfterMuting)
+{
+ [TestProtocol registerWithScheme:@"https"];
+
+ auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]);
+ configuration.get()._mediaCaptureEnabled = YES;
+ auto preferences = [configuration preferences];
+ preferences._mediaCaptureRequiresSecureConnection = NO;
+ preferences._mockCaptureDevicesEnabled = YES;
+ preferences._getUserMediaRequiresFocus = NO;
+
+ auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 320, 500) configuration:configuration.get()]);
+
+ auto observer = adoptNS([[MediaCaptureObserver alloc] init]);
+ [webView addObserver:observer.get() forKeyPath:@"microphoneCaptureState" options:NSKeyValueObservingOptionNew context:nil];
+ [webView addObserver:observer.get() forKeyPath:@"cameraCaptureState" options:NSKeyValueObservingOptionNew context:nil];
+
+ auto delegate = adoptNS([[UserMediaCaptureUIDelegate alloc] init]);
+ [webView setUIDelegate:delegate.get()];
+ [webView _setMediaCaptureReportingDelayForTesting:0];
+
+ [webView synchronouslyLoadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://bundle-file/media-session-capture.html"]]];
+
+ microphoneCaptureStateChange = false;
+ [webView stringByEvaluatingJavaScript:@"startAudioCapture()"];
+ EXPECT_TRUE(waitUntilMicrophoneState(webView.get(), WKMediaCaptureStateActive));
+ EXPECT_EQ(1, [delegate numberOfPrompts]);
+
+ cameraCaptureStateChange = false;
+ [webView stringByEvaluatingJavaScript:@"startVideoCapture()"];
+ EXPECT_TRUE(waitUntilCameraState(webView.get(), WKMediaCaptureStateActive));
+ EXPECT_EQ(2, [delegate numberOfPrompts]);
+
+ cameraCaptureStateChange = false;
+ [webView setCameraCaptureState:WKMediaCaptureStateMuted completionHandler:nil];
+ EXPECT_TRUE(waitUntilCameraState(webView.get(), WKMediaCaptureStateMuted));
+
+ microphoneCaptureStateChange = false;
+ [webView setMicrophoneCaptureState:WKMediaCaptureStateMuted completionHandler:nil];
+ EXPECT_TRUE(waitUntilMicrophoneState(webView.get(), WKMediaCaptureStateMuted));
+
+ microphoneCaptureStateChange = false;
+ [webView stringByEvaluatingJavaScript:@"startAudioCapture()"];
+ EXPECT_TRUE(waitUntilMicrophoneState(webView.get(), WKMediaCaptureStateActive));
+ EXPECT_EQ(3, [delegate numberOfPrompts]);
+
+ cameraCaptureStateChange = false;
+ [webView stringByEvaluatingJavaScript:@"startVideoCapture()"];
+ EXPECT_TRUE(waitUntilCameraState(webView.get(), WKMediaCaptureStateActive));
+ EXPECT_EQ(4, [delegate numberOfPrompts]);
+
+ [webView removeObserver:observer.get() forKeyPath:@"microphoneCaptureState"];
+ [webView removeObserver:observer.get() forKeyPath:@"cameraCaptureState"];
+}
+
+
#if PLATFORM(IOS_FAMILY) && !PLATFORM(MACCATALYST)
TEST(WebKit, GetUserMediaWithWebThread)
{
diff --git a/Tools/TestWebKitAPI/Tests/WebKitCocoa/media-session-capture.html b/Tools/TestWebKitAPI/Tests/WebKitCocoa/media-session-capture.html
index 0996ab2e0415..66fc37c7d535 100644
--- a/Tools/TestWebKitAPI/Tests/WebKitCocoa/media-session-capture.html
+++ b/Tools/TestWebKitAPI/Tests/WebKitCocoa/media-session-capture.html
@@ -28,6 +28,34 @@ function startCapture()
});
}
+function startAudioCapture()
+{
+ navigator.mediaDevices.getUserMedia({ audio:true }).then(stream => {
+ window.actionState = "";
+ video.srcObject = stream;
+
+ if (window.webkit)
+ window.webkit.messageHandlers.gum.postMessage("PASS");
+ }, (e) => {
+ if (window.webkit)
+ window.webkit.messageHandlers.gum.postMessage("FAIL: " + e);
+ });
+}
+
+function startVideoCapture()
+{
+ navigator.mediaDevices.getUserMedia({ video:true }).then(stream => {
+ window.actionState = "";
+ video.srcObject = stream;
+
+ if (window.webkit)
+ window.webkit.messageHandlers.gum.postMessage("PASS");
+ }, (e) => {
+ if (window.webkit)
+ window.webkit.messageHandlers.gum.postMessage("FAIL: " + e);
+ });
+}
+
function startScreenshareCapture()
{
if (!window.internals) {
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: Pawel Wylecial of REDTEAM.PL working with Trend Micro Zero Day Initiative, Ignacio Sanmillan (@ulexec)
Key insight. Message dispatch called into the Client through a non-owning CheckedPtr, so a handler that tore down the client mid-call left a dangling pointer; retaining the client with a RefPtr for the duration of each call fixes it.
Patch Details
diff --git a/Source/WebKit/Platform/IPC/Connection.cpp b/Source/WebKit/Platform/IPC/Connection.cpp
index 6c8a0b8b5439..922888a03343 100644
--- a/Source/WebKit/Platform/IPC/Connection.cpp
+++ b/Source/WebKit/Platform/IPC/Connection.cpp
@@ -1249,7 +1249,7 @@ void Connection::dispatchSyncMessage(Decoder& decoder)
} else
decoder.markInvalid();
} else
- checkedClient()->didReceiveSyncMessage(*this, decoder, replyEncoder);
+ protectedClient()->didReceiveSyncMessage(*this, decoder, replyEncoder);
// If the message was not handled, i.e. replyEncoder was not consumed, reply with cancel
// message. We do not distinquish between a decode failure and failure to find a
@@ -1267,7 +1267,7 @@ void Connection::dispatchDidReceiveInvalidMessage(MessageName messageName, int32
dispatchToClient([protectedThis = Ref { *this }, messageName, indexOfObjectFailingDecoding] {
if (!protectedThis->isValid())
return;
- protectedThis->checkedClient()->didReceiveInvalidMessage(protectedThis, messageName, indexOfObjectFailingDecoding);
+ protectedThis->protectedClient()->didReceiveInvalidMessage(protectedThis, messageName, indexOfObjectFailingDecoding);
});
}
@@ -1276,9 +1276,10 @@ void Connection::dispatchDidCloseAndInvalidate()
dispatchToClient([protectedThis = Ref { *this }] {
// If the connection has been explicitly invalidated before dispatchConnectionDidClose was called,
// then the connection client will be nullptr here.
- if (!protectedThis->m_client)
+ RefPtr client = protectedThis->m_client.get();
+ if (!client)
return;
- protectedThis->checkedClient()->didClose(protectedThis);
+ client->didClose(protectedThis);
protectedThis->invalidate();
});
}
@@ -1314,7 +1315,7 @@ void Connection::enqueueIncomingMessage(UniqueRef<Decoder> incomingMessage)
if (isIncomingMessagesThrottlingEnabled() && m_incomingMessages.size() >= maxPendingIncomingMessagesKillingThreshold) {
m_didRequestProcessTermination = true;
dispatchToClientWithIncomingMessagesLock([protectedThis = Ref { *this }] {
- CheckedPtr client = protectedThis->m_client;
+ RefPtr client = protectedThis->m_client.get();
if (!client)
return;
client->requestRemoteProcessTermination();
@@ -1348,7 +1349,8 @@ void Connection::enqueueIncomingMessage(UniqueRef<Decoder> incomingMessage)
void Connection::dispatchMessage(Decoder& decoder)
{
assertIsCurrent(dispatcher());
- RELEASE_ASSERT(m_client);
+ RefPtr client = m_client.get();
+ RELEASE_ASSERT(client);
if (decoder.messageReceiverName() == ReceiverName::AsyncReply) {
auto handler = takeAsyncReplyHandler(AtomicObjectIdentifier<AsyncReplyIDType>(decoder.destinationID()));
if (!handler) {
@@ -1378,7 +1380,7 @@ void Connection::dispatchMessage(Decoder& decoder)
}
#endif
- checkedClient()->didReceiveMessage(*this, decoder);
+ client->didReceiveMessage(*this, decoder);
}
void Connection::dispatchMessage(UniqueRef<Decoder> message)
@@ -1406,7 +1408,7 @@ void Connection::dispatchMessage(UniqueRef<Decoder> message)
if (m_ignoreInvalidMessageForTesting)
return;
#endif
- checkedClient()->didReceiveInvalidMessage(*this, message->messageName(), message->indexOfObjectFailingDecoding());
+ protectedClient()->didReceiveInvalidMessage(*this, message->messageName(), message->indexOfObjectFailingDecoding());
return;
}
m_inDispatchMessageMarkedToUseFullySynchronousModeForTesting++;
@@ -1458,7 +1460,7 @@ void Connection::dispatchMessage(UniqueRef<Decoder> message)
}
#endif
if (didReceiveInvalidMessage && isValid())
- checkedClient()->didReceiveInvalidMessage(*this, message->messageName(), message->indexOfObjectFailingDecoding());
+ protectedClient()->didReceiveInvalidMessage(*this, message->messageName(), message->indexOfObjectFailingDecoding());
}
size_t Connection::numberOfMessagesToProcess(size_t totalMessages)
diff --git a/Source/WebKit/Platform/IPC/Connection.h b/Source/WebKit/Platform/IPC/Connection.h
index 0c0eb5c21d41..eda198806654 100644
--- a/Source/WebKit/Platform/IPC/Connection.h
+++ b/Source/WebKit/Platform/IPC/Connection.h
@@ -338,7 +338,7 @@ public:
~Connection();
Client* client() const { return m_client.get(); }
- CheckedPtr<Client> checkedClient() const { return m_client; }
+ RefPtr<Client> protectedClient() const { return m_client.get(); }
enum UniqueIDType { };
using UniqueID = AtomicObjectIdentifier<UniqueIDType>;
@@ -884,7 +884,7 @@ template<typename T> Error Connection::waitForAndDispatchImmediately(uint64_t de
return Error::InvalidConnection;
ASSERT(decoderOrError.value()->destinationID() == destinationID);
- checkedClient()->didReceiveMessage(*this, decoderOrError.value());
+ protectedClient()->didReceiveMessage(*this, decoderOrError.value());
return Error::NoError;
}
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: A remote attacker may be able to view leaked DNS queries with Private Relay turned on
Researchers: Mike Cardwell of grepular.com, Bob Lord
Key insight. Creating the DNS resolver directly from the endpoint skipped Network.framework’s path evaluation, so queries egressed outside the Private Relay tunnel; routing resolution through an evaluated nw_path forces DNS to follow the same proxy decision as the connection, which is the actual carrier of the privacy guarantee.
Overview
Impact:
Description: Processing maliciously crafted web content may lead to memory corruption
Researchers: Ignacio Sanmillan (@ulexec)
Key insight. This is a bulk ANGLE dependency roll, not a surgical patch: no single memory-corruption mechanism is isolable from the diff, and the security-relevant fix (most plausibly a shader-translator correctness bug or a Buffer/VertexArray contents-observer lifetime bug) is buried among large autogenerated enum/table churn.
Patch Details
diff --git a/Source/ThirdParty/ANGLE/.gn b/Source/ThirdParty/ANGLE/.gn
index 01e098edbaa6..aa86edf70a35 100644
--- a/Source/ThirdParty/ANGLE/.gn
+++ b/Source/ThirdParty/ANGLE/.gn
@@ -51,8 +51,7 @@ default_args = {
tests_have_location_tags = false
# Use base level API level 29 support for AHardwareBuffer ANGLE tests.
- android32_ndk_api_level = 29
- android64_ndk_api_level = 29
+ android_ndk_api_level = 29
vma_vulkan_headers_dir = "//third_party/vulkan-headers/src"
}
diff --git a/Source/ThirdParty/ANGLE/ANGLE.plist b/Source/ThirdParty/ANGLE/ANGLE.plist
index 66cf3f562231..774bc8030b7c 100644
--- a/Source/ThirdParty/ANGLE/ANGLE.plist
+++ b/Source/ThirdParty/ANGLE/ANGLE.plist
@@ -6,13 +6,13 @@
<key>OpenSourceProject</key>
<string>ANGLE</string>
<key>OpenSourceVersion</key>
- <string>dcbcee8ab32af9ddc7ae1e91c42d995e5281602c</string>
+ <string>b1359277066648c3577ea8b57cc5e0bccee7b6cc</string>
<key>OpenSourceWebsiteURL</key>
<string>http://code.google.com/p/angleproject/</string>
<key>OpenSourceSCM</key>
- <string>git clone https://chromium.googlesource.com/angle/angle && cd angle && git checkout dcbcee8ab32af9ddc7ae1e91c42d995e5281602c</string>
+ <string>git clone https://chromium.googlesource.com/angle/angle && cd angle && git checkout b1359277066648c3577ea8b57cc5e0bccee7b6cc</string>
<key>OpenSourceImportDate</key>
- <string>2025-05-20</string>
+ <string>2025-06-02</string>
<key>OpenSourceLicense</key>
<string>BSD</string>
<key>OpenSourceLicenseFile</key>
diff --git a/Source/ThirdParty/ANGLE/CONTRIBUTORS b/Source/ThirdParty/ANGLE/CONTRIBUTORS
index 95f1f6d4440c..01ca94a9acc1 100644
--- a/Source/ThirdParty/ANGLE/CONTRIBUTORS
+++ b/Source/ThirdParty/ANGLE/CONTRIBUTORS
@@ -176,6 +176,7 @@ Samsung Electronics, Inc.
Colin Peter
Hyunchang Kim
Hyunseok Ko
+ Hyunwoo Yu
Igor Nazarov
Jaedon Lee
Jaewoo Kim
diff --git a/Source/ThirdParty/ANGLE/DEPS b/Source/ThirdParty/ANGLE/DEPS
index 14dbb1517b26..39b3d71f63f3 100644
--- a/Source/ThirdParty/ANGLE/DEPS
+++ b/Source/ThirdParty/ANGLE/DEPS
@@ -43,13 +43,13 @@ vars = {
'checkout_angle_mesa': False,
# Version of Chromium our Chromium-based DEPS are mirrored from.
- 'chromium_revision': 'a343631b79de034de6c3cfc276dd7aca9051d3a5',
+ 'chromium_revision': '055e258e91fc310ded1c0071f108c08f0e924a53',
# We never want to checkout chromium,
# but need a dummy DEPS entry for the autoroller
'dummy_checkout_chromium': False,
# Current revision of VK-GL-CTS (a.k.a dEQP).
- 'vk_gl_cts_revision': 'c9d2e24d1a6da00165a0b5908ea4ba05c2e5f0b2',
+ 'vk_gl_cts_revision': '7be6d4540af28ed9fd82cf7609f4e0442f44043e',
# Current revision of googletest.
# Note: this dep cannot be auto-rolled b/c of nesting.
@@ -88,7 +88,7 @@ vars = {
# Three lines of non-changing comments so that
# the commit queue can handle CLs rolling catapult
# and whatever else without interference from each other.
- 'catapult_revision': '41104509db3116fd5a5e3af4f8242b59de7adc67',
+ 'catapult_revision': '938fc9953b4159851a52022b4f2a90cddedba2fc',
# the commit queue can handle CLs rolling Fuchsia sdk
# and whatever else without interference from each other.
@@ -133,7 +133,7 @@ vars = {
'reclient_version': 're_client_version:0.178.0.5ee9d3e8-gomaip',
# siso CIPD package version.
- 'siso_version': 'git_revision:3753a55d0911b4ea3dd000263088f60edb8d947b',
+ 'siso_version': 'git_revision:d9393c2115244b6e4a797189055e4a2b6769a64d',
# 'magic' text to tell depot_tools that git submodules should be accepted but
# but parity with DEPS file is expected.
@@ -169,6 +169,7 @@ vars = {
'checkout_angle_restricted_trace_animal_crossing': 'checkout_angle_restricted_traces',
'checkout_angle_restricted_trace_antistress': 'checkout_angle_restricted_traces',
'checkout_angle_restricted_trace_antutu_refinery': 'checkout_angle_restricted_traces',
+ 'checkout_angle_restricted_trace_aquarium': 'checkout_angle_restricted_traces',
'checkout_angle_restricted_trace_arena_of_valor': 'checkout_angle_restricted_traces',
'checkout_angle_restricted_trace_arknights': 'checkout_angle_restricted_traces',
'checkout_angle_restricted_trace_asphalt_8': 'checkout_angle_restricted_traces',
@@ -455,12 +456,12 @@ vars = {
deps = {
'build': {
- 'url': Var('chromium_git') + '/chromium/src/build.git@4523ca62d97737c072422ffa49bc64ed6b650fcf',
+ 'url': Var('chromium_git') + '/chromium/src/build.git@69dc3b19a1c8707994411ae29dcaddf3330965b3',
'condition': 'not build_with_chromium',
},
'buildtools': {
- 'url': Var('chromium_git') + '/chromium/src/buildtools.git@e9b36e9ffb02b268d194c3f82f89952d09d0c714',
+ 'url': Var('chromium_git') + '/chromium/src/buildtools.git@cd169314af80e77a75eb25e2c0df8798f35bc04a',
'condition': 'not build_with_chromium',
},
@@ -473,7 +474,7 @@ deps = {
'packages': [
{
'package': 'gn/gn/linux-${{arch}}',
- 'version': 'git_revision:0c25d1bbde6ef17e1e6d3938011f56a00c02e457',
+ 'version': 'git_revision:ebc8f16ca7b0d36a3e532ee90896f9eb48e5423b',
}
],
'dep_type': 'cipd',
@@ -484,7 +485,7 @@ deps = {
'packages': [
{
'package': 'gn/gn/mac-${{arch}}',
- 'version': 'git_revision:0c25d1bbde6ef17e1e6d3938011f56a00c02e457',
+ 'version': 'git_revision:ebc8f16ca7b0d36a3e532ee90896f9eb48e5423b',
}
],
'dep_type': 'cipd',
@@ -506,7 +507,7 @@ deps = {
'packages': [
{
'package': 'gn/gn/windows-amd64',
- 'version': 'git_revision:0c25d1bbde6ef17e1e6d3938011f56a00c02e457',
+ 'version': 'git_revision:ebc8f16ca7b0d36a3e532ee90896f9eb48e5423b',
}
],
'dep_type': 'cipd',
@@ -514,12 +515,12 @@ deps = {
},
'testing': {
- 'url': '{chromium_git}/chromium/src/testing@26a050e9cd2c2c69d85e40345343397e5e7ea0e0',
+ 'url': '{chromium_git}/chromium/src/testing@6a20ae4677a3ee6ed783b576a8586f2566356d22',
'condition': 'not build_with_chromium',
},
'third_party/abseil-cpp': {
- 'url': Var('chromium_git') + '/chromium/src/third_party/abseil-cpp@ec041c739118fb3b4ea96185c1981fbd89ac0b90',
+ 'url': Var('chromium_git') + '/chromium/src/third_party/abseil-cpp@2401283880a451e9ea10787920117fb67695abb4',
'condition': 'not build_with_chromium',
},
@@ -565,7 +566,7 @@ deps = {
'packages': [
{
'package': 'chromium/third_party/android_build_tools/error_prone',
- 'version': 'bd18Z8-1Y85m4C8xYA3z1J_VIhAaE2qRbkrBzn4_200C',
+ 'version': '2jzgic7at7ZxQds7qXCsmkfC4bO7d3qnWU5Q7SFUiJwC',
},
],
'condition': 'checkout_android and not build_with_chromium',
@@ -587,7 +588,7 @@ deps = {
'packages': [
{
'package': 'chromium/third_party/android_build_tools/lint',
- 'version': '7xZFl5M3w3LIJvhKsJYDQoTh5JZ3vr3I_-VWYXiGJC0C',
+ 'version': 'PaYB6553MH9GJfamUZLduJESRbN13Clv2N2beHR6IQAC',
},
],
'condition': 'checkout_android and not build_with_chromium',
@@ -598,7 +599,7 @@ deps = {
'packages': [
{
'package': 'chromium/third_party/android_build_tools/manifest_merger',
- 'version': 'Ym1eWXywKFRvxsgK0yC4bFj9SwZViDqGEPb_w3mAKqMC',
+ 'version': '0L0N3_u2ypIxEEtf0k_l36g_2CykW2BNuZBXm-v5qAUC',
},
],
'condition': 'checkout_android and not build_with_chromium',
@@ -617,7 +618,7 @@ deps = {
},
'third_party/android_deps': {
- 'url': Var('chromium_git') + '/chromium/src/third_party/android_deps@56c2a30762da0f32f799ee5b8af140b1c4a4539a',
+ 'url': Var('chromium_git') + '/chromium/src/third_party/android_deps@4333e5ba900bdefb9cb6e25d60bc35aa6205e95d',
'condition': 'checkout_android and not build_with_chromium',
},
@@ -638,7 +639,7 @@ deps = {
},
'third_party/android_sdk': {
- 'url': Var('chromium_git') + '/chromium/src/third_party/android_sdk@3ad26ca9f48c33ff3d1113c82dd8d54b9a794a72',
+ 'url': Var('chromium_git') + '/chromium/src/third_party/android_sdk@20cf331dc72b447e9178bc10c53069f42cea45bb',
'condition': 'checkout_android and not build_with_chromium',
},
@@ -729,7 +730,7 @@ deps = {
},
'third_party/depot_tools': {
- 'url': Var('chromium_git') + '/chromium/tools/depot_tools.git@f810dae27200e4209bd0cba85bd2266ec0d55312',
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to memory corruption
Researchers: Yuhao Hu, Yan Kang, Chenggang Wu, and Xiaojie Wei
Key insight. The add path cached storage capacity and the new-entry index and bumped the live count before normalizeMapKey, which can reallocate the table — so the write landed at a stale index; reordering the bookkeeping after the side effect fixes it.
Patch Details
diff --git a/Source/JavaScriptCore/runtime/OrderedHashTableHelper.h b/Source/JavaScriptCore/runtime/OrderedHashTableHelper.h
index 8c9190faebc2..ed3de117de16 100644
--- a/Source/JavaScriptCore/runtime/OrderedHashTableHelper.h
+++ b/Source/JavaScriptCore/runtime/OrderedHashTableHelper.h
@@ -439,12 +439,6 @@ public:
Storage* candidate = expandIfNeeded(globalObject, owner, base);
RETURN_IF_EXCEPTION(scope, void());
- Storage& candidateRef = *candidate;
- TableSize capacity = Helper::capacity(candidateRef);
- Entry newEntry = usedCapacity(candidateRef);
- TableIndex newEntryKeyIndex = entryDataStartIndex(dataTableStartIndex(capacity), newEntry);
- incrementAliveEntryCount(candidateRef);
-
bool firstAliveEntry = result.normalizedKey.isEmpty();
if (firstAliveEntry) [[unlikely]] {
result.normalizedKey = normalizeMapKey(key);
@@ -452,6 +446,12 @@ public:
RETURN_IF_EXCEPTION(scope, void());
}
+ Storage& candidateRef = *candidate;
+ TableSize capacity = Helper::capacity(candidateRef);
+ Entry newEntry = usedCapacity(candidateRef);
+ TableIndex newEntryKeyIndex = entryDataStartIndex(dataTableStartIndex(capacity), newEntry);
+ incrementAliveEntryCount(candidateRef);
+
bool rehashed = &base != candidate;
if (rehashed || firstAliveEntry) [[unlikely]]
result.bucketIndex = bucketIndex(capacity, result.hash);
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to memory corruption
Researchers: Yuhao Hu, Yan Kang, Chenggang Wu, and Xiaojie Wei
Key insight. Fast contiguous-array creation skipped the isHavingABadTime() check, so it produced fast-indexed arrays exactly when the prototype’s indexed accessors make that unsafe; adding the check restores the safe fallback.
Patch Details
diff --git a/JSTests/stress/regexp-matches-array-should-respect-have-a-bad-time.js b/JSTests/stress/regexp-matches-array-should-respect-have-a-bad-time.js
new file mode 100644
index 000000000000..0cf373454d20
--- /dev/null
+++ b/JSTests/stress/regexp-matches-array-should-respect-have-a-bad-time.js
@@ -0,0 +1,13 @@
+function F0(){
+ class C4{}
+ function f5(a6,a7,a8){
+ return {__proto__:a7}
+ }
+ f5(C4,C4)
+ Object.defineProperty(C4, 8, {configurable:true, get: f5, set: f5})
+}
+for(let v11=0;v11<5;v11++){
+ v14 = ("size").match(/e/g)
+ new F0()
+ v14[41530] = v14
+}
diff --git a/Source/JavaScriptCore/runtime/JSGlobalObjectInlines.h b/Source/JavaScriptCore/runtime/JSGlobalObjectInlines.h
index bfbf2bd6512a..13c7ce8ae40e 100644
--- a/Source/JavaScriptCore/runtime/JSGlobalObjectInlines.h
+++ b/Source/JavaScriptCore/runtime/JSGlobalObjectInlines.h
@@ -332,6 +332,9 @@ ALWAYS_INLINE JSArray* tryCreateContiguousArrayWithPattern(JSGlobalObject* globa
if (initialLength >= MIN_ARRAY_STORAGE_CONSTRUCTION_LENGTH)
return nullptr;
+ if (globalObject->isHavingABadTime()) [[unlikely]]
+ return nullptr;
+
VM& vm = globalObject->vm();
Structure* structure = globalObject->originalArrayStructureForIndexingType(ArrayWithContiguous);
if (!hasContiguous(structure->indexingType())) [[unlikely]]
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to memory corruption
Researchers: Yuhao Hu, Yan Kang, Chenggang Wu, and Xiaojie Wei
Key insight. OSR-exit availability tracked that a sunken object’s field was flushed to a stack slot but failed to invalidate that flush when a later PutStack/KillStack reused the same slot; the fix is to kill stale heap flushes on slot clobbering so materialization falls back to the true SSA value.
Overview
Impact:
Description: Processing web content may lead to a denial-of-service
Researchers: Yuhao Hu, Yan Kang, Chenggang Wu, and Xiaojie Wei
Key insight. The vulnerability is a broken contract between the Fixup phase and codegen over StringReplace’s search argument: the Fixup phase marked a StringUse edge while codegen materialized it as a generic JSValue under ManualOperandSpeculation, so no single site actually guaranteed the value was a JSString; the fix collapses this into one explicit speculateString site per useKind.
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: Nan Wang (@eternalsakura13) and Ziling Chen
Key insight. An OMG wasm-to-wasm call (notably a tail call to a callee with a smaller argument area) left the stack pointer matching the callee’s layout instead of the caller’s; the one-line fix re-establishes sp = callFrameRegister - frameSize in the call’s link task, restoring the fundamental per-frame SP invariant.
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: Google V8 Security Team
Key insight. The JS-to-Wasm entry thunk must present a valid boxed callee in the frame’s Callee slot before any call that can trigger a stack walk (operationJSToWasmEntryWrapperBuildFrame), and must load/cage the Wasm memory registers only after the frame is built — the fix is purely a reordering plus save/restore of the Callee slot to keep the frame walkable and the memory registers fresh.
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: shandikri working with Trend Micro Zero Day Initiative, Google V8 Security Team
Key insight. A signedness bug: control-flow deltas that are legitimately negative for backward branches were stored unsigned and loaded with a zero-extending 32-bit load, so backward loop/try_table re-entries jumped massively forward instead of back – the fix is simply to make the deltas int32_t and sign-extend them before the 64-bit add.
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: Ignacio Sanmillan (@ulexec)
Key insight. A ref-counted object was cached by raw pointer, so the cache could hand back an object that was already being destroyed; switching the cache to a ThreadSafeWeakPtr (and only self-removing) makes destruction observable and closes the UAF.
Patch Details
diff --git a/Source/WebCore/platform/audio/HRTFDatabaseLoader.cpp b/Source/WebCore/platform/audio/HRTFDatabaseLoader.cpp
index 8ba953418987..5715b0ab7abb 100644
--- a/Source/WebCore/platform/audio/HRTFDatabaseLoader.cpp
+++ b/Source/WebCore/platform/audio/HRTFDatabaseLoader.cpp
@@ -40,9 +40,9 @@
namespace WebCore {
// Keeps track of loaders on a per-sample-rate basis.
-static HashMap<double, HRTFDatabaseLoader*>& loaderMap()
+static HashMap<double, ThreadSafeWeakPtr<HRTFDatabaseLoader>>& loaderMap()
{
- static NeverDestroyed<HashMap<double, HRTFDatabaseLoader*>> loaderMap;
+ static NeverDestroyed<HashMap<double, ThreadSafeWeakPtr<HRTFDatabaseLoader>>> loaderMap;
return loaderMap;
}
@@ -50,13 +50,13 @@ Ref<HRTFDatabaseLoader> HRTFDatabaseLoader::createAndLoadAsynchronouslyIfNecessa
{
ASSERT(isMainThread());
- if (RefPtr<HRTFDatabaseLoader> loader = loaderMap().get(sampleRate)) {
+ if (RefPtr<HRTFDatabaseLoader> loader = loaderMap().get(sampleRate).get()) {
ASSERT(sampleRate == loader->databaseSampleRate());
return loader.releaseNonNull();
}
auto loader = adoptRef(*new HRTFDatabaseLoader(sampleRate));
- loaderMap().add(sampleRate, loader.ptr());
+ loaderMap().set(sampleRate, loader.ptr());
loader->loadAsynchronously();
@@ -76,8 +76,18 @@ HRTFDatabaseLoader::~HRTFDatabaseLoader()
waitForLoaderThreadCompletion();
m_hrtfDatabase = nullptr;
- // Remove ourself from the map.
- loaderMap().remove(m_databaseSampleRate);
+ // Try to remove ourselves from the map, but
+ // not if the cached loader with our sample
+ // rate is not actually the this being destroyed.
+ auto it = loaderMap().find(m_databaseSampleRate);
+ if (it == loaderMap().end())
+ return;
+
+ RefPtr loader = it->value.get();
+ if (loader && loader.get() != this)
+ return;
+
+ loaderMap().remove(it);
}
void HRTFDatabaseLoader::load()
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may disclose sensitive user information
Researchers: Gilad Moav, Yehuda Afek, Anat Bremler-Barr, and Amit Klein
Key insight. The vulnerability was an incomplete security denylist: port 0 was absent from WebKit’s blocked-port list, so a reserved, non-connectable port slipped past the port policy and became a side-channel/probing vector — fixed by adopting the Fetch spec list and explicitly blocking port 0.
Patch Details
diff --git a/LayoutTests/http/tests/security/block-connection-to-zero-port.https-expected.txt b/LayoutTests/http/tests/security/block-connection-to-zero-port.https-expected.txt
new file mode 100644
index 000000000000..08549eb14739
--- /dev/null
+++ b/LayoutTests/http/tests/security/block-connection-to-zero-port.https-expected.txt
@@ -0,0 +1,2 @@
+CONSOLE MESSAGE: Not allowed to use restricted network port 0: https://does-not-exist.com:0/
+This attempts to measure performance based on the zero port.
diff --git a/LayoutTests/http/tests/security/block-connection-to-zero-port.https.html b/LayoutTests/http/tests/security/block-connection-to-zero-port.https.html
new file mode 100644
index 000000000000..045d1a73ce6e
--- /dev/null
+++ b/LayoutTests/http/tests/security/block-connection-to-zero-port.https.html
@@ -0,0 +1,15 @@
+<html>
+<body>
+<script>
+ if (window.testRunner) {
+ testRunner.dumpAsText();
+ testRunner.waitUntilDone();
+ }
+ fetch("https://does-not-exist.com:0/").catch((e) => {
+ const t1 = performance.now();
+ testRunner.notifyDone();
+ });
+</script>
+<p>This attempts to measure performance based on the zero port.</p>
+</body>
+</html>
diff --git a/LayoutTests/platform/gtk-wk2/security/block-test-expected.txt b/LayoutTests/platform/gtk-wk2/security/block-test-expected.txt
new file mode 100644
index 000000000000..27ecb5cf99fd
--- /dev/null
+++ b/LayoutTests/platform/gtk-wk2/security/block-test-expected.txt
@@ -0,0 +1,91 @@
+CONSOLE MESSAGE: Not allowed to use restricted network port 1: http://255.255.255.255:1/test.jpg
+block-test.html - didFinishLoading
+CONSOLE MESSAGE: Not allowed to use restricted network port 7: http://255.255.255.255:7/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 9: http://255.255.255.255:9/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 11: http://255.255.255.255:11/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 13: http://255.255.255.255:13/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 15: http://255.255.255.255:15/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 17: http://255.255.255.255:17/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 19: http://255.255.255.255:19/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 20: http://255.255.255.255:20/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 21: http://255.255.255.255:21/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 22: http://255.255.255.255:22/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 23: http://255.255.255.255:23/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 25: http://255.255.255.255:25/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 37: http://255.255.255.255:37/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 42: http://255.255.255.255:42/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 43: http://255.255.255.255:43/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 53: http://255.255.255.255:53/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 77: http://255.255.255.255:77/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 79: http://255.255.255.255:79/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 87: http://255.255.255.255:87/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 95: http://255.255.255.255:95/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 101: http://255.255.255.255:101/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 102: http://255.255.255.255:102/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 103: http://255.255.255.255:103/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 104: http://255.255.255.255:104/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 109: http://255.255.255.255:109/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 110: http://255.255.255.255:110/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 111: http://255.255.255.255:111/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 113: http://255.255.255.255:113/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 115: http://255.255.255.255:115/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 117: http://255.255.255.255:117/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 119: http://255.255.255.255:119/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 123: http://255.255.255.255:123/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 135: http://255.255.255.255:135/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 139: http://255.255.255.255:139/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 143: http://255.255.255.255:143/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 179: http://255.255.255.255:179/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 389: http://255.255.255.255:389/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 465: http://255.255.255.255:465/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 512: http://255.255.255.255:512/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 513: http://255.255.255.255:513/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 514: http://255.255.255.255:514/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 515: http://255.255.255.255:515/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 526: http://255.255.255.255:526/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 530: http://255.255.255.255:530/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 531: http://255.255.255.255:531/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 532: http://255.255.255.255:532/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 540: http://255.255.255.255:540/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 548: http://255.255.255.255:548/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 556: http://255.255.255.255:556/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 563: http://255.255.255.255:563/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 587: http://255.255.255.255:587/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 601: http://255.255.255.255:601/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 636: http://255.255.255.255:636/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 993: http://255.255.255.255:993/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 995: http://255.255.255.255:995/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 2049: http://255.255.255.255:2049/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 3659: http://255.255.255.255:3659/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 4045: http://255.255.255.255:4045/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 4190: http://255.255.255.255:4190/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 6000: http://255.255.255.255:6000/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 6665: http://255.255.255.255:6665/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 6666: http://255.255.255.255:6666/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 6667: http://255.255.255.255:6667/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 6668: http://255.255.255.255:6668/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 6669: http://255.255.255.255:6669/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 6679: http://255.255.255.255:6679/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 6697: http://255.255.255.255:6697/test.jpg
+http://255.255.255.255:65535/test.jpg - willSendRequest <NSURLRequest URL http://255.255.255.255:65535/test.jpg, main document URL block-test.html, http method GET> redirectResponse (null)
+http://255.255.255.255:65535/test.jpg - didFailLoadingWithError: <NSError domain g-io-error-quark, code 38, failing URL "http://255.255.255.255:65535/test.jpg">
+CONSOLE MESSAGE: Not allowed to use restricted network port 0: http://255.255.255.255:0/test.jpg
+http://255.255.255.255/test.jpg - willSendRequest <NSURLRequest URL http://255.255.255.255/test.jpg, main document URL block-test.html, http method GET> redirectResponse (null)
+http://255.255.255.255/test.jpg - didFailLoadingWithError: <NSError domain g-io-error-quark, code 38, failing URL "http://255.255.255.255/test.jpg">
+http://255.255.255.255/test.jpg - willSendRequest <NSURLRequest URL http://255.255.255.255/test.jpg, main document URL block-test.html, http method GET> redirectResponse (null)
+http://255.255.255.255/test.jpg - didFailLoadingWithError: <NSError domain g-io-error-quark, code 38, failing URL "http://255.255.255.255/test.jpg">
+ftp://255.255.255.255/test.jpg - willSendRequest <NSURLRequest URL ftp://255.255.255.255/test.jpg, main document URL block-test.html, http method GET> redirectResponse (null)
+CONSOLE MESSAGE: FTP URLs are disabled
+CONSOLE MESSAGE: Cannot load image ftp://255.255.255.255/test.jpg due to access control checks.
+ftp://255.255.255.255/test.jpg - didFailLoadingWithError: <NSError domain WebKitInternal, code 0, failing URL "ftp://255.255.255.255/test.jpg">
+ftp://255.255.255.255/test.jpg - willSendRequest <NSURLRequest URL ftp://255.255.255.255/test.jpg, main document URL block-test.html, http method GET> redirectResponse (null)
+CONSOLE MESSAGE: FTP URLs are disabled
+CONSOLE MESSAGE: Cannot load image ftp://255.255.255.255/test.jpg due to access control checks.
+ftp://255.255.255.255/test.jpg - didFailLoadingWithError: <NSError domain WebKitInternal, code 0, failing URL "ftp://255.255.255.255/test.jpg">
+ftp://255.255.255.255:22/test.jpg - willSendRequest <NSURLRequest URL ftp://255.255.255.255:22/test.jpg, main document URL block-test.html, http method GET> redirectResponse (null)
+CONSOLE MESSAGE: FTP URLs are disabled
+CONSOLE MESSAGE: Cannot load image ftp://255.255.255.255:22/test.jpg due to access control checks.
+ftp://255.255.255.255:22/test.jpg - didFailLoadingWithError: <NSError domain WebKitInternal, code 0, failing URL "ftp://255.255.255.255:22/test.jpg">
+This test attempts to change the src of an IMG tag to all blocked ports to confirm that WebKit returns the correct error for them - blocked instead of cannot find. It also tries the FTP ports for exemptions. Due to the nature of this test, the results can only be processed automatically via DumpRenderTree
+
+
diff --git a/LayoutTests/platform/wpe/security/block-test-expected.txt b/LayoutTests/platform/wpe/security/block-test-expected.txt
new file mode 100644
index 000000000000..27ecb5cf99fd
--- /dev/null
+++ b/LayoutTests/platform/wpe/security/block-test-expected.txt
@@ -0,0 +1,91 @@
+CONSOLE MESSAGE: Not allowed to use restricted network port 1: http://255.255.255.255:1/test.jpg
+block-test.html - didFinishLoading
+CONSOLE MESSAGE: Not allowed to use restricted network port 7: http://255.255.255.255:7/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 9: http://255.255.255.255:9/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 11: http://255.255.255.255:11/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 13: http://255.255.255.255:13/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 15: http://255.255.255.255:15/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 17: http://255.255.255.255:17/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 19: http://255.255.255.255:19/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 20: http://255.255.255.255:20/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 21: http://255.255.255.255:21/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 22: http://255.255.255.255:22/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 23: http://255.255.255.255:23/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 25: http://255.255.255.255:25/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 37: http://255.255.255.255:37/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 42: http://255.255.255.255:42/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 43: http://255.255.255.255:43/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 53: http://255.255.255.255:53/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 77: http://255.255.255.255:77/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 79: http://255.255.255.255:79/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 87: http://255.255.255.255:87/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 95: http://255.255.255.255:95/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 101: http://255.255.255.255:101/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 102: http://255.255.255.255:102/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 103: http://255.255.255.255:103/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 104: http://255.255.255.255:104/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 109: http://255.255.255.255:109/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 110: http://255.255.255.255:110/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 111: http://255.255.255.255:111/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 113: http://255.255.255.255:113/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 115: http://255.255.255.255:115/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 117: http://255.255.255.255:117/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 119: http://255.255.255.255:119/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 123: http://255.255.255.255:123/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 135: http://255.255.255.255:135/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 139: http://255.255.255.255:139/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 143: http://255.255.255.255:143/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 179: http://255.255.255.255:179/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 389: http://255.255.255.255:389/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 465: http://255.255.255.255:465/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 512: http://255.255.255.255:512/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 513: http://255.255.255.255:513/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 514: http://255.255.255.255:514/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 515: http://255.255.255.255:515/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 526: http://255.255.255.255:526/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 530: http://255.255.255.255:530/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 531: http://255.255.255.255:531/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 532: http://255.255.255.255:532/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 540: http://255.255.255.255:540/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 548: http://255.255.255.255:548/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 556: http://255.255.255.255:556/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 563: http://255.255.255.255:563/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 587: http://255.255.255.255:587/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 601: http://255.255.255.255:601/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 636: http://255.255.255.255:636/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 993: http://255.255.255.255:993/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 995: http://255.255.255.255:995/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 2049: http://255.255.255.255:2049/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 3659: http://255.255.255.255:3659/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 4045: http://255.255.255.255:4045/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 4190: http://255.255.255.255:4190/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 6000: http://255.255.255.255:6000/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 6665: http://255.255.255.255:6665/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 6666: http://255.255.255.255:6666/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 6667: http://255.255.255.255:6667/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 6668: http://255.255.255.255:6668/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 6669: http://255.255.255.255:6669/test.jpg
+CONSOLE MESSAGE: Not allowed to use restricted network port 6679: http://255.255.255.255:6679/test.jpg
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Visiting a malicious website may lead to address bar spoofing
Researchers: Jaydev Ahire
Key insight. Transient, page-anchored UI (an open picker) was allowed to survive a page-open transition because createNewPage() never dismissed it, letting the displayed UI drift out of sync with the address bar; the fix simply forces picker dismissal before the new page takes over the view.
Patch Details
diff --git a/Source/WebKit/UIProcess/PageClient.h b/Source/WebKit/UIProcess/PageClient.h
index abe91cdbb12d..26060904e694 100644
--- a/Source/WebKit/UIProcess/PageClient.h
+++ b/Source/WebKit/UIProcess/PageClient.h
@@ -331,6 +331,7 @@ public:
completionHandler(makeUnexpected(WebCore::ExceptionData { WebCore::ExceptionCode::NotSupportedError, "Digital credentials are not supported."_s }));
}
virtual void dismissDigitalCredentialsPicker(WTF::CompletionHandler<void(bool)>&& completionHandler) { completionHandler(true); }
+ virtual void dismissAnyOpenPicker() { }
virtual void didChangeContentSize(const WebCore::IntSize&) = 0;
diff --git a/Source/WebKit/UIProcess/WebPageProxy.cpp b/Source/WebKit/UIProcess/WebPageProxy.cpp
index ca618f862099..c42687a93ccd 100644
--- a/Source/WebKit/UIProcess/WebPageProxy.cpp
+++ b/Source/WebKit/UIProcess/WebPageProxy.cpp
@@ -8678,6 +8678,9 @@ void WebPageProxy::createNewPage(IPC::Connection& connection, WindowFeatures&& w
return;
}
+ if (RefPtr pageClient = this->pageClient())
+ pageClient->dismissAnyOpenPicker();
+
newPage->setOpenedByDOM();
if (openerAppInitiatedState)
diff --git a/Source/WebKit/UIProcess/ios/PageClientImplIOS.h b/Source/WebKit/UIProcess/ios/PageClientImplIOS.h
index 2d3a6d98d1cf..49f14eaaa303 100644
--- a/Source/WebKit/UIProcess/ios/PageClientImplIOS.h
+++ b/Source/WebKit/UIProcess/ios/PageClientImplIOS.h
@@ -218,6 +218,8 @@ private:
void dismissDigitalCredentialsPicker(WTF::CompletionHandler<void(bool)>&&) override;
#endif
+ void dismissAnyOpenPicker() override;
+
void disableDoubleTapGesturesDuringTapIfNecessary(WebKit::TapIdentifier) override;
void handleSmartMagnificationInformationForPotentialTap(WebKit::TapIdentifier, const WebCore::FloatRect& renderRect, bool fitEntireRect, double viewportMinimumScale, double viewportMaximumScale, bool nodeIsRootLevel, bool nodeIsPluginElement) override;
diff --git a/Source/WebKit/UIProcess/ios/PageClientImplIOS.mm b/Source/WebKit/UIProcess/ios/PageClientImplIOS.mm
index 5b54dc660c52..be31742fb9de 100644
--- a/Source/WebKit/UIProcess/ios/PageClientImplIOS.mm
+++ b/Source/WebKit/UIProcess/ios/PageClientImplIOS.mm
@@ -41,6 +41,7 @@
#import "NativeWebKeyboardEvent.h"
#import "NavigationState.h"
#import "PDFPluginIdentifier.h"
+#import "PickerDismissalReason.h"
#import "PlatformXRSystem.h"
#import "RemoteLayerTreeNode.h"
#import "RunningBoardServicesSPI.h"
@@ -771,6 +772,11 @@ void PageClientImpl::dismissDigitalCredentialsPicker(CompletionHandler<void(bool
}
#endif
+void PageClientImpl::dismissAnyOpenPicker()
+{
+ [contentView() dismissPickersIfNeededWithReason:WebKit::PickerDismissalReason::ViewRemoved];
+}
+
void PageClientImpl::showInspectorHighlight(const WebCore::InspectorOverlay::Highlight& highlight)
{
[contentView() _showInspectorHighlight:highlight];
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to universal cross site scripting
Researchers: Martin Bajanik of Fingerprint, Ammar Askar
Key insight. The fix closes a gap in the web-archive isolation work: local .webarchive loads honored the isolation data-store swap but not macOS quarantine/Gatekeeper, so a downloaded, unapproved archive could still execute and achieve UXSS; the guard now cancels the navigation when the file is quarantined and not user-approved.
Overview
Impact:
Description: A download’s origin may be incorrectly associated
Researchers: Syarif Muhammad Sajjad
Key insight. Downloads arising from history traversal (targetItem) or client/user-input navigations have no legitimate web-content originating frame, yet the old code still attributed them to a prior frame’s origin; the fix passes std::nullopt in exactly those cases so no false origin is associated.
Patch Details
diff --git a/Source/WebKit/UIProcess/WebPageProxy.cpp b/Source/WebKit/UIProcess/WebPageProxy.cpp
index 7258568bbfd6..f736411a38ed 100644
--- a/Source/WebKit/UIProcess/WebPageProxy.cpp
+++ b/Source/WebKit/UIProcess/WebPageProxy.cpp
@@ -5118,7 +5118,13 @@ void WebPageProxy::receivedPolicyDecision(PolicyAction action, API::Navigation*
std::optional<DownloadID> downloadID;
if (action == PolicyAction::Download) {
// Create a download proxy.
- auto download = m_configuration->protectedProcessPool()->createDownloadProxy(m_websiteDataStore, navigationAction->request(), downloadOriginatingPage(navigation).ptr(), navigation ? navigation->originatingFrameInfo() : std::optional(navigationAction->data().originatingFrameInfoData));
+ RefPtr<DownloadProxy> download;
+
+ if (navigation && (navigation->targetItem() || navigation->isRequestFromClientOrUserInput()))
+ download = m_configuration->protectedProcessPool()->createDownloadProxy(m_websiteDataStore, navigationAction->request(), downloadOriginatingPage(navigation).ptr(), std::nullopt);
+ else
+ download = m_configuration->protectedProcessPool()->createDownloadProxy(m_websiteDataStore, navigationAction->request(), downloadOriginatingPage(navigation).ptr(), navigation ? navigation->originatingFrameInfo() : std::optional(navigationAction->data().originatingFrameInfoData));
+
download->setDidStartCallback([weakThis = WeakPtr { *this }, navigationAction = WTFMove(navigationAction)] (auto* downloadProxy) {
RefPtr protectedThis = weakThis.get();
if (!protectedThis || !downloadProxy)
@@ -5158,7 +5164,13 @@ void WebPageProxy::receivedNavigationResponsePolicyDecision(WebCore::PolicyActio
std::optional<DownloadID> downloadID;
if (action == PolicyAction::Download) {
- auto download = m_configuration->protectedProcessPool()->createDownloadProxy(m_websiteDataStore, request, downloadOriginatingPage(navigation).ptr(), navigation ? navigation->originatingFrameInfo() : std::nullopt);
+ RefPtr<DownloadProxy> download;
+
+ if (navigation && (navigation->targetItem() || navigation->isRequestFromClientOrUserInput()))
+ download = m_configuration->protectedProcessPool()->createDownloadProxy(m_websiteDataStore, request, downloadOriginatingPage(navigation).ptr(), std::nullopt);
+ else
+ download = m_configuration->protectedProcessPool()->createDownloadProxy(m_websiteDataStore, request, downloadOriginatingPage(navigation).ptr(), navigation ? navigation->originatingFrameInfo() : std::nullopt);
+
download->setDidStartCallback([weakThis = WeakPtr { *this }, navigationResponse = WTFMove(navigationResponse)] (auto* downloadProxy) {
RefPtr protectedThis = weakThis.get();
if (!protectedThis || !downloadProxy)
diff --git a/Tools/TestWebKitAPI/Tests/WebKitCocoa/Download.mm b/Tools/TestWebKitAPI/Tests/WebKitCocoa/Download.mm
index 041298c0d6f2..c96a4e3b170a 100644
--- a/Tools/TestWebKitAPI/Tests/WebKitCocoa/Download.mm
+++ b/Tools/TestWebKitAPI/Tests/WebKitCocoa/Download.mm
@@ -3257,12 +3257,25 @@ TEST(WKDownload, OriginatingFrameWhenConvertingNavigationInNewWindow)
return openedWebView.get();
};
+ auto frameInfoShouldBeEqual = ^(WKFrameInfo *a, WKFrameInfo *b) {
+ EXPECT_EQ(a.isMainFrame, b.isMainFrame);
+ EXPECT_WK_STREQ(a.request.URL.absoluteString, b.request.URL.absoluteString);
+ EXPECT_WK_STREQ(a.securityOrigin.protocol, b.securityOrigin.protocol);
+ EXPECT_WK_STREQ(a.securityOrigin.host, b.securityOrigin.host);
+ EXPECT_EQ(a.securityOrigin.port, b.securityOrigin.port);
+ EXPECT_EQ(a.webView, b.webView);
+ };
+
+ __block bool isClientOrUserInitiated = false;
__block bool checkedDownload { false };
+
auto tryOpenerInitiatedDownloads = ^{
checkedDownload = false;
+ isClientOrUserInitiated = true;
[webView evaluateJavaScript:@"a = document.createElement('a'); a.href = 'https://webkit.org/download'; a.target = '_blank'; document.body.appendChild(a); a.click()" completionHandler:nil];
Util::run(&checkedDownload);
+ isClientOrUserInitiated = false;
checkedDownload = false;
[webView evaluateJavaScript:@"w = window.open('https://webkit.org/download')" completionHandler:nil];
Util::run(&checkedDownload);
@@ -3282,7 +3295,15 @@ TEST(WKDownload, OriginatingFrameWhenConvertingNavigationInNewWindow)
};
navigationDelegate.get().navigationResponseDidBecomeDownload = ^(WKNavigationResponse *response, WKDownload *download) {
frameInfoShouldBeEqual(response._navigationInitiatingFrame, openerMainFrame.get());
- frameInfoShouldBeEqual(download.originatingFrame, openerMainFrame.get());
+
+ if (isClientOrUserInitiated) {
+ EXPECT_WK_STREQ(download.originatingFrame.request.URL.absoluteString, "about:blank");
+ EXPECT_WK_STREQ(download.originatingFrame.securityOrigin.host, "");
+
+ isClientOrUserInitiated = false;
+ } else
+ frameInfoShouldBeEqual(download.originatingFrame, openerMainFrame.get());
+
checkedDownload = true;
};
tryOpenerInitiatedDownloads();
@@ -3337,4 +3358,106 @@ TEST(WKDownload, OriginatingFrameWhenNavigatingToNewDomainWithRedirect)
TestWebKitAPI::Util::run(&checkedDownload);
}
+TEST(WKDownload, OriginatingFrameHostWhenDownloadComesFromGoBackNavigation)
+{
+ HTTPServer server({
+ { "/firstSite"_s, { "firstSite"_s } },
+ { "/secondSite"_s, { "secondSite"_s } }
+ }, HTTPServer::Protocol::HttpsProxy);
+
+ RetainPtr configuration = server.httpsProxyConfiguration();
+ RetainPtr webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 300, 300) configuration:configuration.get()]);
+
+ RetainPtr navigationDelegate = adoptNS([TestNavigationDelegate new]);
+ [navigationDelegate allowAnyTLSCertificate];
+ [webView setNavigationDelegate:navigationDelegate.get()];
+
+ [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://firstSite.com/firstSite"]]];
+ [navigationDelegate waitForDidFinishNavigation];
+
+ [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://secondSite.com/secondSite"]]];
+ [navigationDelegate waitForDidFinishNavigation];
+
+ RetainPtr downloadDelegate = adoptNS([TestDownloadDelegate new]);
+ NSURL *expectedDownloadFile = tempFileThatDoesNotExist();
+ __block bool downloadDestinationDecided = false;
+
+ downloadDelegate.get().decideDestinationUsingResponse = ^(WKDownload *download, NSURLResponse *, NSString *, void (^completionHandler)(NSURL *)) {
+ EXPECT_STREQ(download.originatingFrame.securityOrigin.host.UTF8String, "");
+
+ downloadDestinationDecided = true;
+ completionHandler(expectedDownloadFile);
+ };
+
+ navigationDelegate.get().decidePolicyForNavigationAction = ^(WKNavigationAction *, void (^completionHandler)(WKNavigationActionPolicy)) {
+ completionHandler(WKNavigationActionPolicyDownload);
+ };
+
+ navigationDelegate.get().decidePolicyForNavigationResponse = ^(WKNavigationResponse *, void (^completionHandler)(WKNavigationResponsePolicy)) {
+ completionHandler(WKNavigationResponsePolicyDownload);
+ };
+
+ navigationDelegate.get().navigationActionDidBecomeDownload = ^(WKNavigationAction *, WKDownload *download) {
+ download.delegate = downloadDelegate.get();
+ };
+
+ navigationDelegate.get().navigationResponseDidBecomeDownload = ^(WKNavigationResponse *, WKDownload *download) {
+ download.delegate = downloadDelegate.get();
+ };
+
+ [webView goBack];
+ Util::run(&downloadDestinationDecided);
+}
+
+TEST(WKDownload, OriginatingFrameHostWhenDownloadComesFromClientInputNavigation)
+{
+ HTTPServer server({
+ { "/firstSite"_s, { "firstSite"_s } },
+ { "/secondSite"_s, { "secondSite"_s } },
+ { "/thirdSite"_s, { "thirdSite"_s } }
+ }, HTTPServer::Protocol::HttpsProxy);
+
+ RetainPtr configuration = server.httpsProxyConfiguration();
+ RetainPtr webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 300, 300) configuration:configuration.get()]);
+
+ RetainPtr navigationDelegate = adoptNS([TestNavigationDelegate new]);
+ [navigationDelegate allowAnyTLSCertificate];
+ [webView setNavigationDelegate:navigationDelegate.get()];
+
+ [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://firstSite.com/firstSite"]]];
+ [navigationDelegate waitForDidFinishNavigation];
+
+ [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://secondSite.com/secondSite"]]];
+ [navigationDelegate waitForDidFinishNavigation];
+
+ RetainPtr downloadDelegate = adoptNS([TestDownloadDelegate new]);
+ __block bool downloadDestinationDecided = false;
+
+ downloadDelegate.get().decideDestinationUsingResponse = ^(WKDownload *download, NSURLResponse *, NSString *, void (^completionHandler)(NSURL *)) {
+ EXPECT_STREQ(download.originatingFrame.securityOrigin.host.UTF8String, "");
+
+ downloadDestinationDecided = true;
+ completionHandler(tempFileThatDoesNotExist());
+ };
+
+ navigationDelegate.get().decidePolicyForNavigationAction = ^(WKNavigationAction *, void (^completionHandler)(WKNavigationActionPolicy)) {
+ completionHandler(WKNavigationActionPolicyDownload);
+ };
+
+ navigationDelegate.get().decidePolicyForNavigationResponse = ^(WKNavigationResponse *, void (^completionHandler)(WKNavigationResponsePolicy)) {
+ completionHandler(WKNavigationResponsePolicyDownload);
+ };
+
+ navigationDelegate.get().navigationActionDidBecomeDownload = ^(WKNavigationAction *, WKDownload *download) {
+ download.delegate = downloadDelegate.get();
+ };
+
+ navigationDelegate.get().navigationResponseDidBecomeDownload = ^(WKNavigationResponse *, WKDownload *download) {
+ download.delegate = downloadDelegate.get();
+ };
+
+ [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://thirdSite.com/thirdSite"]]];
+ Util::run(&downloadDestinationDecided);
+}
+
}
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may disclose internal states of the app
Researchers: HexRabbit (@h3xr4bb1t) from DEVCORE Research Team
Key insight. The YARR-JIT surrogate-pair index advance used a fragile sentinel-conditional to set the extra read size, which could be left wrong and push the match index out of bounds, leaking memory; using a fixed advance of 1 removes the desync.
Patch Details
diff --git a/Source/JavaScriptCore/yarr/YarrJIT.cpp b/Source/JavaScriptCore/yarr/YarrJIT.cpp
index 2ec28e58f53c..4dafe9f18ac2 100644
--- a/Source/JavaScriptCore/yarr/YarrJIT.cpp
+++ b/Source/JavaScriptCore/yarr/YarrJIT.cpp
@@ -92,15 +92,6 @@ static_assert(areCanonicallyEquivalentCharArgReg == GPRInfo::returnValueGPR);
#endif
#endif
-#if ENABLE(YARR_JIT_UNICODE_EXPRESSIONS) && ENABLE(YARR_JIT_UNICODE_CAN_INCREMENT_INDEX_FOR_NON_BMP)
-// This enhancement allows us to advance the index by 2 when we read a non-BMP surrogate pair, but fail to match.
-// The way it works is that we initialize the firstCharacterAdditionalReadSize register to an initial sentinal value.
-// When reading a possible surrogate pair, we change firstCharacterAdditionalReadSize from the sentinal to 0 if we read
-// a BMP (16-bit) character or 1 if the read value is a non-BMP. Once changed from the sentinel value, we don't change
-// again during the next read. We add firstCharacterAdditionalReadSize to index for the next iteration on a failed
-// match and when setting the the possible new match start location.
-constexpr static int32_t additionalReadSizeSentinel = 0x4;
-#endif
WTF_MAKE_TZONE_ALLOCATED_IMPL(BoyerMooreBitmap);
WTF_MAKE_TZONE_ALLOCATED_IMPL(BoyerMooreFastCandidates);
@@ -364,23 +355,14 @@ void tryReadUnicodeCharImpl(VM& vm, CCallHelpers& jit, MacroAssembler::RegisterI
#endif
#if ENABLE(YARR_JIT_UNICODE_CAN_INCREMENT_INDEX_FOR_NON_BMP)
- if constexpr (useNonBMPOptimization == TryReadUnicodeCharGenFirstNonBMPOptimization::UseOptimization) {
- // If this is the first read of the alternation, set additional read size to 1 because we got a non-BMP code point.
- jit.moveConditionallyTest32(MacroAssembler::NonZero, regs.firstCharacterAdditionalReadSize, MacroAssembler::TrustedImm32(additionalReadSizeSentinel), ARM64Registers::zr, regs.firstCharacterAdditionalReadSize);
- jit.addOneConditionally32(MacroAssembler::NonZero, regs.firstCharacterAdditionalReadSize, regs.firstCharacterAdditionalReadSize);
- }
+ if (useNonBMPOptimization == TryReadUnicodeCharGenFirstNonBMPOptimization::UseOptimization)
+ jit.move(MacroAssembler::TrustedImm32(1), regs.firstCharacterAdditionalReadSize);
#endif
done.append(jit.jump());
isBMP.link(jit);
jit.and32(MacroAssembler::TrustedImm32(0xffff), resultReg);
-#if ENABLE(YARR_JIT_UNICODE_CAN_INCREMENT_INDEX_FOR_NON_BMP)
- if constexpr (useNonBMPOptimization == TryReadUnicodeCharGenFirstNonBMPOptimization::UseOptimization) {
- // If this is the first read of the alternation, set additional read size to 0.
- jit.moveConditionallyTest32(MacroAssembler::NonZero, regs.firstCharacterAdditionalReadSize, MacroAssembler::TrustedImm32(additionalReadSizeSentinel), ARM64Registers::zr, regs.firstCharacterAdditionalReadSize);
- }
-#endif
done.append(jit.jump());
slowCases.link(&jit);
@@ -455,13 +437,6 @@ void tryReadUnicodeCharSlowImpl(CCallHelpers& jit)
isBMP.link(jit);
jit.and32(MacroAssembler::TrustedImm32(0xffff), resultReg);
-#if ENABLE(YARR_JIT_UNICODE_CAN_INCREMENT_INDEX_FOR_NON_BMP)
- if constexpr (useNonBMPOptimization == TryReadUnicodeCharGenFirstNonBMPOptimization::UseOptimization) {
- // If this is the first read of the alternation, set additional read size to 0.
- jit.moveConditionallyTest32(MacroAssembler::NonZero, regs.firstCharacterAdditionalReadSize, MacroAssembler::TrustedImm32(additionalReadSizeSentinel), ARM64Registers::zr, regs.firstCharacterAdditionalReadSize);
- }
-#endif
-
jit.ret();
checkForDanglingSurrogates.link(&jit);
@@ -495,13 +470,6 @@ void tryReadUnicodeCharSlowImpl(CCallHelpers& jit)
bmpDone.link(&jit);
-#if ENABLE(YARR_JIT_UNICODE_CAN_INCREMENT_INDEX_FOR_NON_BMP)
- if constexpr (useNonBMPOptimization == TryReadUnicodeCharGenFirstNonBMPOptimization::UseOptimization) {
- // If this is the first read of the alternation, set additional read size to 0.
- jit.moveConditionallyTest32(MacroAssembler::NonZero, regs.firstCharacterAdditionalReadSize, MacroAssembler::TrustedImm32(additionalReadSizeSentinel), ARM64Registers::zr, regs.firstCharacterAdditionalReadSize);
- }
-#endif
-
haveResult.link(&jit);
}
@@ -3233,6 +3201,12 @@ class YarrGenerator final : public YarrJITInfo {
termMatchTargets.append(MatchTargets());
+#if ENABLE(YARR_JIT_UNICODE_EXPRESSIONS) && ENABLE(YARR_JIT_UNICODE_CAN_INCREMENT_INDEX_FOR_NON_BMP)
+ // Always set to zero.
+ if (m_useFirstNonBMPCharacterOptimization)
+ m_jit.move(MacroAssembler::TrustedImm32(0), m_regs.firstCharacterAdditionalReadSize);
+#endif
+
// Upon entry at the head of the set of alternatives, check if input is available
// to run the first alternative. (This progresses the input position).
op.m_jumps.append(jumpIfNoAvailableInput(alternative->m_minimumSize));
@@ -3241,11 +3215,6 @@ class YarrGenerator final : public YarrJITInfo {
// set as appropriate to this alternative.
op.m_reentry = m_jit.label();
-#if ENABLE(YARR_JIT_UNICODE_EXPRESSIONS) && ENABLE(YARR_JIT_UNICODE_CAN_INCREMENT_INDEX_FOR_NON_BMP)
- // Clear first character read size so it can be set on the first read.
- if (m_useFirstNonBMPCharacterOptimization)
- m_jit.move(MacroAssembler::TrustedImm32(additionalReadSizeSentinel), m_regs.firstCharacterAdditionalReadSize);
-#endif
// Emit fast skip path with stride if we have BoyerMooreInfo.
if (op.m_bmInfo) {
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: A type confusion issue could lead to memory corruption
Researchers: Google V8 Security Team
Key insight. The bug is an FP-environment scoping problem: WebAudio’s denormal guard flushed denormals to zero (and was absent/uncontrolled on ARM), and that non-IEEE FP mode could bleed into JIT-compiled JavaScript on the audio thread, breaking the optimizer’s floating-point assumptions into a type confusion. The fix makes the mode change precisely saved/restored and architecture-correct.
Overview
Impact:
Description: Processing maliciously crafted web content may lead to memory corruption
Researchers: rheza (@ginggilBesel), Edouard Bochin (@le_douds) and Tao Yan (@Ga1ois) of Palo Alto Networks
Background
Advisory: Apple Security Advisory
| Attribute | Value |
|---|---|
| CVE | CVE-2025-24223 |
| Bugzilla | 287577 |
| Component | WebKit |
| Bug Class | LogicError |
| Severity | medium |
| Advisory | Apple Advisory |
Fix not public. The WebKit fix for this bug is embargoed or not yet disclosed. The bug ID is confirmed from the Apple advisory, but no public commit references it.
Overview
Impact:
Description: Processing maliciously crafted web content may lead to memory corruption
Researchers: Nan Wang(@eternalsakura13)
Key insight. The array bounds check was 32-bit but the element address used the full 64-bit index register, so a 64-bit index with in-bounds low bits bypassed the check; zero-extending the index after the check ties the access to the validated value.
Patch Details
diff --git a/JSTests/wasm/stress/array-get-large-i64-index.js b/JSTests/wasm/stress/array-get-large-i64-index.js
new file mode 100644
index 000000000000..3ac909fa2ebb
--- /dev/null
+++ b/JSTests/wasm/stress/array-get-large-i64-index.js
@@ -0,0 +1,10 @@
+var wasm_code = new Uint8Array([
+ 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x89, 0x80, 0x80, 0x80, 0x00, 0x02, 0x5e, 0x78, 0x01, 0x60, 0x01, 0x7e, 0x01, 0x7f, 0x03, 0x82, 0x80, 0x80, 0x80, 0x00, 0x01, 0x01, 0x07, 0x88, 0x80, 0x80, 0x80, 0x00, 0x01, 0x04, 0x6d, 0x61, 0x69, 0x6e, 0x00, 0x00, 0x0a, 0x95, 0x80, 0x80, 0x80, 0x00, 0x01, 0x8f, 0x80, 0x80, 0x80, 0x00, 0x00, 0x41, 0x00, 0x41, 0x10, 0xfb, 0x06, 0x00, 0x20, 0x00, 0xa7, 0xfb, 0x0d, 0x00, 0x0b,
+]);
+var wasm_module = new WebAssembly.Module(wasm_code);
+var wasm_instance = new WebAssembly.Instance(wasm_module);
+var f = wasm_instance.exports.main;
+for (let i = 0; i < 10000; ++i) {
+ f(0x7fffffff00000000n);
+}
+
diff --git a/Source/JavaScriptCore/wasm/WasmBBQJIT64.cpp b/Source/JavaScriptCore/wasm/WasmBBQJIT64.cpp
index ecaeaf0590d3..e5c5e81a3bc4 100644
--- a/Source/JavaScriptCore/wasm/WasmBBQJIT64.cpp
+++ b/Source/JavaScriptCore/wasm/WasmBBQJIT64.cpp
@@ -1817,6 +1817,7 @@ PartialResult WARN_UNUSED_RETURN BBQJIT::addArrayGet(ExtGCOpType arrayGetKind, u
indexLocation = loadIfNecessary(index);
throwExceptionIf(ExceptionType::OutOfBoundsArrayGet,
m_jit.branch32(MacroAssembler::AboveOrEqual, indexLocation.asGPR(), MacroAssembler::Address(arrayLocation.asGPR(), JSWebAssemblyArray::offsetOfSize())));
+ m_jit.zeroExtend32ToWord(indexLocation.asGPR(), indexLocation.asGPR());
}
emitArrayGetPayload(elementType, arrayLocation.asGPR(), wasmScratchGPR);
@@ -2001,6 +2002,7 @@ PartialResult WARN_UNUSED_RETURN BBQJIT::addArraySet(uint32_t typeIndex, Express
Location indexLocation = loadIfNecessary(index);
throwExceptionIf(ExceptionType::OutOfBoundsArraySet,
m_jit.branch32(MacroAssembler::AboveOrEqual, indexLocation.asGPR(), MacroAssembler::Address(arrayLocation.asGPR(), JSWebAssemblyArray::offsetOfSize())));
+ m_jit.zeroExtend32ToWord(indexLocation.asGPR(), indexLocation.asGPR());
}
emitArraySetUnchecked(typeIndex, arrayref, index, value);
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: A malicious website may exfiltrate data cross-origin
Researchers: Ivan Fratric of Google Project Zero
Key insight. canAccessRules() fell open (returned true) when a stylesheet had no owner document, and CSSOM wrappers weren’t given the correct origin-clean flag, so cross-origin stylesheet rules became readable — an SOP bypass; the fix denies access when the owner document is gone and propagates the real origin-clean state.
Patch Details
diff --git a/LayoutTests/http/tests/security/access-cssstylesheet-after-removing-from-document-expected.txt b/LayoutTests/http/tests/security/access-cssstylesheet-after-removing-from-document-expected.txt
new file mode 100644
index 000000000000..cc77e09161e7
--- /dev/null
+++ b/LayoutTests/http/tests/security/access-cssstylesheet-after-removing-from-document-expected.txt
@@ -0,0 +1,33 @@
+Tests accessing CSSStyleSheet after removing from the document
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+PASS () => stylesheet.cssRules did not throw exception.
+PASS () => stylesheet.insertRule('#element2 { margin: 10px; }') did not throw exception.
+PASS () => stylesheet.deleteRule(0) did not throw exception.
+PASS () => stylesheet.cssRules threw exception SecurityError: Not allowed to access cross-origin stylesheet.
+PASS () => stylesheet.insertRule('body { margin: 10px; } ') threw exception SecurityError: Not allowed to insert rule into cross-origin stylesheet.
+PASS () => stylesheet.deleteRule(0) threw exception SecurityError: Not allowed to delete rule from cross-origin stylesheet.
+PASS () => stylesheet.cssRules did not throw exception.
+PASS () => stylesheet.insertRule('#element2 { margin: 10px; }') did not throw exception.
+PASS () => stylesheet.deleteRule(0) did not throw exception.
+PASS () => stylesheet.cssRules did not throw exception.
+PASS () => stylesheet.insertRule('#element2 { margin: 10px; }') did not throw exception.
+PASS () => stylesheet.deleteRule(0) did not throw exception.
+PASS () => stylesheet.cssRules threw exception SecurityError: Not allowed to access cross-origin stylesheet.
+PASS () => stylesheet.insertRule('body { margin: 10px; } ') threw exception SecurityError: Not allowed to insert rule into cross-origin stylesheet.
+PASS () => stylesheet.deleteRule(0) threw exception SecurityError: Not allowed to delete rule from cross-origin stylesheet.
+PASS () => stylesheet.cssRules threw exception SecurityError: Not allowed to access cross-origin stylesheet.
+PASS () => stylesheet.insertRule('body { margin: 10px; } ') threw exception SecurityError: Not allowed to insert rule into cross-origin stylesheet.
+PASS () => stylesheet.deleteRule(0) threw exception SecurityError: Not allowed to delete rule from cross-origin stylesheet.
+PASS () => stylesheet.cssRules did not throw exception.
+PASS () => stylesheet.insertRule('#element2 { margin: 10px; }') did not throw exception.
+PASS () => stylesheet.deleteRule(0) did not throw exception.
+PASS () => stylesheet.cssRules threw exception SecurityError: Not allowed to access cross-origin stylesheet.
+PASS () => stylesheet.insertRule('body { margin: 10px; } ') threw exception SecurityError: Not allowed to insert rule into cross-origin stylesheet.
+PASS () => stylesheet.deleteRule(0) threw exception SecurityError: Not allowed to delete rule from cross-origin stylesheet.
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/http/tests/security/access-cssstylesheet-after-removing-from-document.html b/LayoutTests/http/tests/security/access-cssstylesheet-after-removing-from-document.html
new file mode 100644
index 000000000000..4be151a8584c
--- /dev/null
+++ b/LayoutTests/http/tests/security/access-cssstylesheet-after-removing-from-document.html
@@ -0,0 +1,93 @@
+<!DOCTYPE html>
+
+<head>
+
+<title>Accessing CSSStyleSheet after removing from document</title>
+<script src="/resources/js-test-pre.js"></script>
+
+<!--
+ Explicitly use <link> here instead of dynamically creating <link> elements
+ in JavaScript, so they're loaded before the JavaScript runs.
+-->
+
+<!-- Inline style -->
+<style id="local-style">#element { margin: 10px; }</style>
+
+<!-- Same origin -->
+<link class="test-stylesheet"
+ rel="stylesheet"
+ href="/security/resources/cssStyle.css"
+ data-accessible="true">
+
+<!-- Cross origin, no CORS -->
+<link class="test-stylesheet"
+ rel="stylesheet"
+ href="http://localhost:8000/security/resources/cssStyle.css"
+ data-accessible="false">
+
+<!-- Cross origin, CORS succeeds -->
+<link class="test-stylesheet"
+ rel="stylesheet"
+ href="http://localhost:8000/security/resources/xorigincss1-allow-star.py"
+ crossorigin=""
+ data-accessible="true">
+
+<!-- Same origin -> same origin -->
+<link class="test-stylesheet"
+ rel="stylesheet"
+ href="/resources/redirect.py?url=http://127.0.0.1:8000/security/resources/cssStyle.css"
+ data-accessible="true">
+
+<!-- Same origin -> cross origin, no CORS -->
+<link class="test-stylesheet"
+ rel="stylesheet"
+ href="/resources/redirect.py?url=http://localhost:8000/security/resources/cssStyle.css"
+ data-accessible="false">
+
+<!-- Cross origin -> same origin, no CORS -->
+<link class="test-stylesheet"
+ rel="stylesheet"
+ href="http://localhost:8000/resources/redirect.py?url=http://127.0.0.1:8000/security/resources/cssStyle.css"
+ data-accessible="false">
+
+<!-- Cross origin -> same origin, CORS succeeds -->
+<link class="test-stylesheet"
+ rel="stylesheet"
+ href="http://localhost:8000/security/resources/redirect-allow-star.py?url=http://127.0.0.1:8000/security/resources/xorigincss1-allow-star.py"
+ crossorigin=""
+ data-accessible="true">
+
+<!-- Same origin -> cross origin -> same origin -->
+<link class="test-stylesheet"
+ rel="stylesheet"
+ href="http://127.0.0.1:8000/resources/redirect.py?url=http://localhost:8000/resources/redirect.py?url=http://127.0.0.1:8000/security/resources/cssStyle.css"
+ data-accessible="false">
+
+</head>
+
+<body>
+
+<script>
+ description("Tests accessing CSSStyleSheet after removing from the document");
+
+ const link_elements = Array.from(document.getElementsByClassName("test-stylesheet"));
+ for (const element of link_elements) {
+ const stylesheet = element.sheet;
+
+ window.document.head.removeChild(element);
+
+ if (element.dataset.accessible === "true") {
+ shouldNotThrow(() => stylesheet.cssRules);
+ shouldNotThrow(() => stylesheet.insertRule('#element2 { margin: 10px; }'));
+ shouldNotThrow(() => stylesheet.deleteRule(0));
+ } else {
+ shouldThrowErrorName(() => stylesheet.cssRules, "SecurityError");
+ shouldThrowErrorName(() => stylesheet.insertRule('body { margin: 10px; } '), "SecurityError");
+ shouldThrowErrorName(() => stylesheet.deleteRule(0), "SecurityError");
+ }
+ }
+</script>
+
+<script src="/resources/js-test-post.js"></script>
+
+</body>
diff --git a/LayoutTests/http/tests/security/access-imported-cssstylesheet-after-removing-from-document-expected.txt b/LayoutTests/http/tests/security/access-imported-cssstylesheet-after-removing-from-document-expected.txt
new file mode 100644
index 000000000000..321f2c3eab2a
--- /dev/null
+++ b/LayoutTests/http/tests/security/access-imported-cssstylesheet-after-removing-from-document-expected.txt
@@ -0,0 +1,24 @@
+Tests accessing @import-ed CSSStyleSheet after
+
+PASS () => stylesheet.cssRules did not throw exception.
+PASS () => stylesheet.insertRule('#element2 { margin: 10px; }') did not throw exception.
+PASS () => stylesheet.deleteRule(0) did not throw exception.
+PASS () => stylesheet.cssRules threw exception SecurityError: Not allowed to access cross-origin stylesheet.
+PASS () => stylesheet.insertRule('body { margin: 10px; } ') threw exception SecurityError: Not allowed to insert rule into cross-origin stylesheet.
+PASS () => stylesheet.deleteRule(0) threw exception SecurityError: Not allowed to delete rule from cross-origin stylesheet.
+PASS () => stylesheet.cssRules did not throw exception.
+PASS () => stylesheet.insertRule('#element2 { margin: 10px; }') did not throw exception.
+PASS () => stylesheet.deleteRule(0) did not throw exception.
+PASS () => stylesheet.cssRules threw exception SecurityError: Not allowed to access cross-origin stylesheet.
+PASS () => stylesheet.insertRule('body { margin: 10px; } ') threw exception SecurityError: Not allowed to insert rule into cross-origin stylesheet.
+PASS () => stylesheet.deleteRule(0) threw exception SecurityError: Not allowed to delete rule from cross-origin stylesheet.
+PASS () => stylesheet.cssRules threw exception SecurityError: Not allowed to access cross-origin stylesheet.
+PASS () => stylesheet.insertRule('body { margin: 10px; } ') threw exception SecurityError: Not allowed to insert rule into cross-origin stylesheet.
+PASS () => stylesheet.deleteRule(0) threw exception SecurityError: Not allowed to delete rule from cross-origin stylesheet.
+PASS () => stylesheet.cssRules threw exception SecurityError: Not allowed to access cross-origin stylesheet.
+PASS () => stylesheet.insertRule('body { margin: 10px; } ') threw exception SecurityError: Not allowed to insert rule into cross-origin stylesheet.
+PASS () => stylesheet.deleteRule(0) threw exception SecurityError: Not allowed to delete rule from cross-origin stylesheet.
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/http/tests/security/access-imported-cssstylesheet-after-removing-from-document.html b/LayoutTests/http/tests/security/access-imported-cssstylesheet-after-removing-from-document.html
new file mode 100644
index 000000000000..d396755fe6b0
--- /dev/null
+++ b/LayoutTests/http/tests/security/access-imported-cssstylesheet-after-removing-from-document.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+
+<head>
+
+<title>Accessing @import-ed CSSStyleSheet after removing from document</title>
+<script src="/resources/js-test-pre.js"></script>
+
+<style id="local-style">
+ /* Rule 0: same origin */
+ @import url('/security/resources/cssStyle.css');
+
+ /* Rule 1: cross origin */
+ @import url('http://localhost:8000/security/resources/cssStyle.css');
+
+ /* Rule 3: same origin -> same origin */
+ @import url('/resources/redirect.py?url=http://127.0.0.1:8000/security/resources/cssStyle.css');
+
+ /* Rule 4: same origin -> cross origin */
+ @import url('/resources/redirect.py?url=http://localhost:8000/security/resources/cssStyle.css');
+
+ /* Rule 5: cross origin -> same origin */
+ @import url('http://localhost:8000/resources/redirect.py?url=http://127.0.0.1:8000/security/resources/cssStyle.css');
+
+ /* Rule 6: same origin -> cross origin -> same origin */
+ @import url('http://127.0.0.1:8000/resources/redirect.py?url=http://localhost:8000/resources/redirect.py?url=http://127.0.0.1:8000/security/resources/cssStyle.css');
+</style>
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: Yuhao Hu, Yan Kang, Chenggang Wu, Xiaojie Wei
Key insight. GetGlobalObject was validated only as a cell (KnownCellUse) when it actually requires a JSObject; the missing ObjectUse speculation let a non-object cell be treated as an object and its ‘structure’ dereferenced.
Patch Details
diff --git a/JSTests/stress/dfg-get-global-object-should-use-object-edge.js b/JSTests/stress/dfg-get-global-object-should-use-object-edge.js
new file mode 100644
index 000000000000..70262b4304ce
--- /dev/null
+++ b/JSTests/stress/dfg-get-global-object-should-use-object-edge.js
@@ -0,0 +1,12 @@
+//@ runDefault("--validateOptions=true", "--thresholdForJITSoon=10", "--thresholdForJITAfterWarmUp=10", "--thresholdForOptimizeAfterWarmUp=100", "--thresholdForOptimizeAfterLongWarmUp=100", "--thresholdForOptimizeSoon=100", "--thresholdForFTLOptimizeAfterWarmUp=1000", "--thresholdForFTLOptimizeSoon=1000", "--validateBCE=true", "--useConcurrentJIT=0")
+
+function opt(arg) {
+ try {
+ arg.test({ test: "padEnd" });
+ } catch (e) { }
+ arg.test(/123/);
+}
+
+for (let i = 0; i < 50; i++) {
+ opt({ test: opt });
+}
diff --git a/Source/JavaScriptCore/dfg/DFGFixupPhase.cpp b/Source/JavaScriptCore/dfg/DFGFixupPhase.cpp
index f394063c4549..a13dcaf41580 100644
--- a/Source/JavaScriptCore/dfg/DFGFixupPhase.cpp
+++ b/Source/JavaScriptCore/dfg/DFGFixupPhase.cpp
@@ -2115,12 +2115,16 @@ private:
case SkipScope:
case GetScope:
case GetGetter:
- case GetSetter:
- case GetGlobalObject: {
+ case GetSetter: {
fixEdge<KnownCellUse>(node->child1());
break;
}
+ case GetGlobalObject: {
+ fixEdge<ObjectUse>(node->child1());
+ break;
+ }
+
case UnwrapGlobalProxy: {
fixEdge<GlobalProxyUse>(node->child1());
break;
diff --git a/Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp b/Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp
index e9b92d24abe5..8080a2ec33e9 100644
--- a/Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp
+++ b/Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp
@@ -7966,9 +7966,15 @@ void SpeculativeJIT::compileGetGlobalObject(Node* node)
{
SpeculateCellOperand object(this, node->child1());
GPRTemporary result(this);
- emitLoadStructure(vm(), object.gpr(), result.gpr());
- loadPtr(Address(result.gpr(), Structure::globalObjectOffset()), result.gpr());
- cellResult(result.gpr(), node);
+
+ GPRReg objectGPR = object.gpr();
+ GPRReg resultGPR = result.gpr();
+
+ speculateObject(node->child1(), objectGPR);
+
+ emitLoadStructure(vm(), objectGPR, resultGPR);
+ loadPtr(Address(resultGPR, Structure::globalObjectOffset()), resultGPR);
+ cellResult(resultGPR, node);
}
void SpeculativeJIT::compileGetGlobalThis(Node* node)
diff --git a/Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp b/Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp
index 2899e69ebb78..a4db4022121c 100644
--- a/Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp
+++ b/Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp
@@ -11749,7 +11749,7 @@ IGNORE_CLANG_WARNINGS_END
void compileGetGlobalObject()
{
- LValue structure = loadStructure(lowCell(m_node->child1()));
+ LValue structure = loadStructure(lowObject(m_node->child1()));
setJSValue(m_out.loadPtr(structure, m_heaps.Structure_globalObject));
}
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: Jiming Wang and Jikai Ren
Key insight. The abstract interpreter under-specified the result structure of single-child NewArrayWithSpread (only recognizing the PhantomNewArrayBuffer-contiguous case as CopyOnWrite) while FTL always allocates a CopyOnWrite contiguous array, so the AI’s model diverged from the emitted allocation; the fix unifies both the lowering and the AI so the proven structure always matches what is built.
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: Ignacio Sanmillan (@ulexec)
Key insight. A hard-coded depth of 1 in the 3D-texture CPU mip-generation call ignored the previous level’s real depth, so any 3D texture whose depth has more mip levels than its width/height drove the downsampling kernel out of bounds; passing prevLevelDepth restores the correct dimension.
Patch Details
diff --git a/Source/ThirdParty/ANGLE/src/libANGLE/renderer/metal/TextureMtl.mm b/Source/ThirdParty/ANGLE/src/libANGLE/renderer/metal/TextureMtl.mm
index 0763d958a143..ad105b75faac 100644
--- a/Source/ThirdParty/ANGLE/src/libANGLE/renderer/metal/TextureMtl.mm
+++ b/Source/ThirdParty/ANGLE/src/libANGLE/renderer/metal/TextureMtl.mm
@@ -1743,9 +1743,10 @@ angle::Result TextureMtl::generateMipmapCPU(const gl::Context *context)
}
// Generate mip level
- angleFormat.mipGenerationFunction(
- prevLevelWidth, prevLevelHeight, 1, prevLevelData.get(), prevLevelRowPitch,
- prevLevelDepthPitch, dstLevelData.get(), dstRowPitch, dstDepthPitch);
+ angleFormat.mipGenerationFunction(prevLevelWidth, prevLevelHeight, prevLevelDepth,
+ prevLevelData.get(), prevLevelRowPitch,
+ prevLevelDepthPitch, dstLevelData.get(), dstRowPitch,
+ dstDepthPitch);
mtl::MipmapNativeLevel nativeLevel = mViewFromBaseToMaxLevel->getNativeLevel(glLevel);
diff --git a/Source/ThirdParty/ANGLE/src/tests/gl_tests/MipmapTest.cpp b/Source/ThirdParty/ANGLE/src/tests/gl_tests/MipmapTest.cpp
index 182f77671e65..d0f14b7701a3 100644
--- a/Source/ThirdParty/ANGLE/src/tests/gl_tests/MipmapTest.cpp
+++ b/Source/ThirdParty/ANGLE/src/tests/gl_tests/MipmapTest.cpp
@@ -1503,6 +1503,49 @@ TEST_P(MipmapTestES3, MipmapForDeepTextureArray)
EXPECT_PIXEL_COLOR_EQ(px, py, GLColor::red);
}
+// Creates a mipmapped 3D texture with 1x1x2 size, GL_ALPHA, and calls GenerateMipmap. Should not
+// crash. Tests the case where the format is non-renderable and depth has more mip levels than
+// width, height.
+TEST_P(MipmapTestES3, MipmapsForTexture3DNonRenderableMoreMipLevelsInDepthNoCrash)
+{
+ int px = getWindowWidth() / 2;
+ int py = getWindowHeight() / 2;
+
+ glUseProgram(m3DProgram);
+ EXPECT_GL_NO_ERROR();
+
+ glBindTexture(GL_TEXTURE_3D, mTexture);
+ uint8_t colors[] = {5, 200};
+ glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
+ glTexImage3D(GL_TEXTURE_3D, 0, GL_ALPHA, 1, 1, 2, 0, GL_ALPHA, GL_UNSIGNED_BYTE, &colors);
+ glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST);
+ glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ EXPECT_GL_NO_ERROR();
+ glGenerateMipmap(GL_TEXTURE_3D);
+ EXPECT_GL_NO_ERROR();
+
+ // Mipmap level 0 slice 0.
+ glUniform1f(mTexture3DLODUniformLocation, 0.);
+ glUniform1f(mTexture3DSliceUniformLocation, 0.25f);
+ drawQuad(m3DProgram, "position", 0.5f);
+ EXPECT_GL_NO_ERROR();
+ EXPECT_PIXEL_COLOR_EQ(px, py, GLColor(0, 0, 0, 5));
+ glClear(GL_COLOR_BUFFER_BIT);
+
+ // Mipmap level 0 slice 1.
+ glUniform1f(mTexture3DSliceUniformLocation, 0.75f);
+ drawQuad(m3DProgram, "position", 0.5f);
+ EXPECT_GL_NO_ERROR();
+ EXPECT_PIXEL_COLOR_EQ(px, py, GLColor(0, 0, 0, 200));
+
+ // Mipmap level 1 slice 0, the only slice.
+ glUniform1f(mTexture3DLODUniformLocation, 1.);
+ glUniform1f(mTexture3DSliceUniformLocation, 0.);
+ drawQuad(m3DProgram, "position", 0.5f);
+ EXPECT_GL_NO_ERROR();
+ EXPECT_PIXEL_COLOR_NEAR(px, py, GLColor(0, 0, 0, 102), 1);
+}
+
// Creates a mipmapped 3D texture with two layers, and calls ANGLE's GenerateMipmap.
// Then tests if the mipmaps are rendered correctly for all two layers.
TEST_P(MipmapTestES3, MipmapsForTexture3D)
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to memory corruption
Researchers: Andreas Jaegersberger & Ro Achterberg of Nosebeard Labs
Key insight. The fast stringifier’s capacity check computed 1 + stringLength*6 + 1 in a 32-bit domain, so a huge string wrapped the check to a small value and the escaped output overran the buffer; an overflow-checked length restores the guard.
Patch Details
diff --git a/JSTests/stress/fast-stringifier-check-string-length.js b/JSTests/stress/fast-stringifier-check-string-length.js
new file mode 100644
index 000000000000..43f81c13407e
--- /dev/null
+++ b/JSTests/stress/fast-stringifier-check-string-length.js
@@ -0,0 +1,8 @@
+//@ skip if $memoryLimited
+var string = "\x00".repeat(715827883);
+try {
+ JSON.stringify(string);
+} catch { }
+try {
+ JSON.stringify(string);
+} catch { }
diff --git a/Source/JavaScriptCore/runtime/JSONObject.cpp b/Source/JavaScriptCore/runtime/JSONObject.cpp
index 00714548f0ff..ace6278f26ca 100644
--- a/Source/JavaScriptCore/runtime/JSONObject.cpp
+++ b/Source/JavaScriptCore/runtime/JSONObject.cpp
@@ -1294,7 +1294,12 @@ void FastStringifier<CharType, bufferMode>::append(JSValue value)
}
}
- if (!hasRemainingCapacity(1 + static_cast<size_t>(stringLength) * 6 + 1)) [[unlikely]] {
+ auto escapedLength = 1 + CheckedUint32 { stringLength } * 6 + 1;
+ if (escapedLength.hasOverflowed()) [[unlikely]] {
+ recordBufferFull();
+ return;
+ }
+ if (!hasRemainingCapacity(escapedLength.value())) [[unlikely]] {
recordBufferFull();
return;
}
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to memory corruption
Researchers: wac working with Trend Micro Zero Day Initiative
Key insight. A C++ object (ScrollerMac) was referenced by AppKit-owned Objective-C helpers (delegate and animations) whose lifetimes it did not control, via raw back-pointers; re-attaching the scroller without invalidating the old delegate left those helpers dangling. The fix pairs lifetime-checked CheckedPtr back-pointers with an explicit invalidate() on delegate replacement.
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: Juergen Schmied of Lynck GmbH
Key insight. The bug is a classic WebKit lifetime/type-assumption error: a raw Page* was held across a re-entrant contentsResized() call and a page’s main frame was assumed local, so a suspended-cache or site-isolated page could leave code touching freed or wrong-typed objects; the fix is a RefPtr plus the localMainFrame() accessor.
Overview
Impact:
Description: Loading a malicious iframe may lead to a cross-site scripting attack
Researchers: Muhammad Zaid Ghifari (Mr.ZheeV) and Kalimantan Utara
Key insight. A javascript: URL passed to window.open could be loaded through the createNewPage path and execute in the wrong origin; refusing javascript: loads while createNewPage is in progress blocks the XSS.
Patch Details
diff --git a/Source/WebKit/UIProcess/WebPageProxy.cpp b/Source/WebKit/UIProcess/WebPageProxy.cpp
index 1ed72a79e47b..9c880f410184 100644
--- a/Source/WebKit/UIProcess/WebPageProxy.cpp
+++ b/Source/WebKit/UIProcess/WebPageProxy.cpp
@@ -1978,6 +1978,11 @@ RefPtr<API::Navigation> WebPageProxy::loadRequest(WebCore::ResourceRequest&& req
WEBPAGEPROXY_RELEASE_LOG(Loading, "loadRequest:");
+ if (m_isCallingCreateNewPage && request.url().protocolIsJavaScript()) {
+ WEBPAGEPROXY_RELEASE_LOG(Loading, "loadRequest: Not loading javascript URL during createNewPage.");
+ return nullptr;
+ }
+
if (!hasRunningProcess())
launchProcess(Site { request.url() }, ProcessLaunchReason::InitialProcess);
@@ -8407,6 +8412,7 @@ void WebPageProxy::createNewPage(IPC::Connection& connection, WindowFeatures&& w
openedBlobURL,
wantsNoOpener = windowFeatures.wantsNoOpener()
] (RefPtr<WebPageProxy> newPage) mutable {
+ m_isCallingCreateNewPage = false;
if (!newPage) {
reply(std::nullopt, std::nullopt);
return;
@@ -8481,6 +8487,7 @@ void WebPageProxy::createNewPage(IPC::Connection& connection, WindowFeatures&& w
}
trySOAuthorization(configuration.copyRef(), WTFMove(navigationAction), *this, WTFMove(completionHandler), [this, protectedThis = Ref { *this }, configuration] (Ref<API::NavigationAction>&& navigationAction, CompletionHandler<void(RefPtr<WebPageProxy>&&)>&& completionHandler) mutable {
+ m_isCallingCreateNewPage = true;
m_uiClient->createNewPage(*this, WTFMove(configuration), WTFMove(navigationAction), WTFMove(completionHandler));
});
}
diff --git a/Source/WebKit/UIProcess/WebPageProxy.h b/Source/WebKit/UIProcess/WebPageProxy.h
index e59999486597..fb0f5231ef06 100644
--- a/Source/WebKit/UIProcess/WebPageProxy.h
+++ b/Source/WebKit/UIProcess/WebPageProxy.h
@@ -3403,6 +3403,8 @@ private:
String m_failingProvisionalLoadURL;
bool m_isLoadingAlternateHTMLStringForFailingProvisionalLoad { false };
+ bool m_isCallingCreateNewPage { false };
+
std::unique_ptr<WebPageLoadTiming> m_pageLoadTiming;
HashSet<WebCore::FrameIdentifier> m_framesWithSubresourceLoadingForPageLoadTiming;
RunLoop::Timer m_generatePageLoadTimingTimer;
diff --git a/Tools/TestWebKitAPI/Tests/WebKitCocoa/OpenAndCloseWindow.mm b/Tools/TestWebKitAPI/Tests/WebKitCocoa/OpenAndCloseWindow.mm
index 470859c0c5d1..c5b9f945482e 100644
--- a/Tools/TestWebKitAPI/Tests/WebKitCocoa/OpenAndCloseWindow.mm
+++ b/Tools/TestWebKitAPI/Tests/WebKitCocoa/OpenAndCloseWindow.mm
@@ -26,16 +26,21 @@
#import "config.h"
#import "DeprecatedGlobalValues.h"
+#import "HTTPServer.h"
#import "PlatformUtilities.h"
+#import "Test.h"
#import "TestNavigationDelegate.h"
#import "TestUIDelegate.h"
#import "TestWKWebView.h"
+#import <WebKit/WKFrameInfo.h>
+#import <WebKit/WKFrameInfoPrivate.h>
#import <WebKit/WKNavigationActionPrivate.h>
#import <WebKit/WKPreferences.h>
#import <WebKit/WKUIDelegatePrivate.h>
#import <WebKit/WKWebViewConfiguration.h>
#import <WebKit/WKWebViewPrivate.h>
#import <WebKit/WKWindowFeaturesPrivate.h>
+#import <WebKit/_WKFrameTreeNode.h>
#import <wtf/RetainPtr.h>
@class OpenAndCloseWindowUIDelegate;
@@ -451,6 +456,33 @@ TEST(WebKit, TryClose)
EXPECT_FALSE([webView _tryClose]);
}
+TEST(WebKit, TryWindowOpenJavascriptURLInIframeSingleWindowApp)
+{
+ TestWebKitAPI::HTTPServer server({
+ { "/mainframe"_s, { "<iframe src='https://domain2.com/subframe'></iframe>"_s } },
+ { "/subframe"_s, { ""_s } }
+ }, TestWebKitAPI::HTTPServer::Protocol::HttpsProxy);
+
+ auto navigationDelegate = adoptNS([TestNavigationDelegate new]);
+ [navigationDelegate allowAnyTLSCertificate];
+ auto webView = adoptNS([TestWKWebView new]);
+ webView.get().configuration.preferences.javaScriptCanOpenWindowsAutomatically = YES;
+ auto uiDelegate = adoptNS([TestUIDelegate new]);
+ [webView setUIDelegate:uiDelegate.get()];
+ webView.get().navigationDelegate = navigationDelegate.get();
+
+ __block bool calledCreateWebViewWithConfiguration { false };
+ uiDelegate.get().createWebViewWithConfiguration = ^WKWebView *(WKWebViewConfiguration *, WKNavigationAction *navigationAction, WKWindowFeatures *) {
+ EXPECT_NULL([webView loadRequest:[navigationAction request]]);
+ calledCreateWebViewWithConfiguration = true;
+ return nil;
+ };
+
+ [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://domain1.com/mainframe"]]];
+ [webView objectByEvaluatingJavaScript:@"window.open(\"javascript:alert('test');\")" inFrame:[webView firstChildFrame]];
+ TestWebKitAPI::Util::run(&calledCreateWebViewWithConfiguration);
+}
+
TEST(WEBKIT, NavigationActionHasOpener)
{
auto runHasOpenerTest = [](NSString *js, bool expectsOpener) {
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: Francisco Alonso (@revskills), and an anonymous researcher
Key insight. The transform fast path validated only the 9-character minimum for ‘rotate(0)’ but the translate branch reads index 11, so short ’translate’ inputs read out of bounds; a per-branch length check fixes it.
Patch Details
diff --git a/LayoutTests/fast/css/transform-translate-parsing-crash-expected.txt b/LayoutTests/fast/css/transform-translate-parsing-crash-expected.txt
new file mode 100644
index 000000000000..2985393944b6
--- /dev/null
+++ b/LayoutTests/fast/css/transform-translate-parsing-crash-expected.txt
@@ -0,0 +1 @@
+This test passes if it doesn't crash
diff --git a/LayoutTests/fast/css/transform-translate-parsing-crash.html b/LayoutTests/fast/css/transform-translate-parsing-crash.html
new file mode 100644
index 000000000000..aa3dd7fa63f2
--- /dev/null
+++ b/LayoutTests/fast/css/transform-translate-parsing-crash.html
@@ -0,0 +1,7 @@
+<div id=t>This test passes if it doesn't crash</div>
+<script>
+if (window.testRunner)
+ testRunner.dumpAsText();
+t.style.transform = "translatez";
+t.style.transform = "translate3d";
+</script>
diff --git a/Source/WebCore/css/parser/CSSParserFastPaths.cpp b/Source/WebCore/css/parser/CSSParserFastPaths.cpp
index c692b9543476..abcc2d5b5595 100644
--- a/Source/WebCore/css/parser/CSSParserFastPaths.cpp
+++ b/Source/WebCore/css/parser/CSSParserFastPaths.cpp
@@ -835,12 +835,12 @@ static bool parseTransformNumberArguments(CharType*& pos, CharType* end, unsigne
return true;
}
-static constexpr int kShortestValidTransformStringLength = 9; // "rotate(0)"
-
template <typename CharType>
static RefPtr<CSSFunctionValue> parseSimpleTransformValue(CharType*& pos, CharType* end)
{
- if (end - pos < kShortestValidTransformStringLength)
+ // Also guarantees indexes up to pos[8] are safe to access below.
+ constexpr auto shortestValidTransformStringLength = 9; // "rotate(0)"
+ if (end - pos < shortestValidTransformStringLength)
return nullptr;
bool isTranslate = toASCIILower(pos[0]) == 't'
@@ -854,6 +854,11 @@ static RefPtr<CSSFunctionValue> parseSimpleTransformValue(CharType*& pos, CharTy
&& toASCIILower(pos[8]) == 'e';
if (isTranslate) {
+ // Also guarantees indexes up to pos[11] are safe to access below.
+ constexpr auto shortestValidTranslateStringLength = 12; // "translate(0)"
+ if (end - pos < shortestValidTranslateStringLength)
+ return nullptr;
+
CSSValueID transformType;
unsigned expectedArgumentCount = 1;
unsigned argumentStart = 11;
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: Paul Bakker of ParagonERP
Key insight. CSS nesting could be resolved more than once against the same sheet, re-processing selectors whose owners had been destroyed; making resolution once-per-sheet (with a mutation reset) and asserting selector liveness fixes the use-after-destroy.
Patch Details
diff --git a/LayoutTests/fast/selectors/has-nesting-crash-expected.txt b/LayoutTests/fast/selectors/has-nesting-crash-expected.txt
new file mode 100644
index 000000000000..d380fbf76e1a
--- /dev/null
+++ b/LayoutTests/fast/selectors/has-nesting-crash-expected.txt
@@ -0,0 +1,2 @@
+This test passes if it doesn't crash.
+
diff --git a/LayoutTests/fast/selectors/has-nesting-crash.html b/LayoutTests/fast/selectors/has-nesting-crash.html
new file mode 100644
index 000000000000..63dba7c79e31
--- /dev/null
+++ b/LayoutTests/fast/selectors/has-nesting-crash.html
@@ -0,0 +1,18 @@
+<body>
+This test passes if it doesn't crash.
+<host-element id=host>
+<template shadowrootmode=open></template>
+</host-element>
+<script>
+window.testRunner?.dumpAsText();
+
+const css = new CSSStyleSheet();
+css.replaceSync("* { :has(.foo) { color: green; } }");
+document.adoptedStyleSheets.push(css);
+host.shadowRoot.adoptedStyleSheets.push(css);
+document.body.offsetWidth;
+
+const div = document.createElement("div");
+div.classList.add("foo");
+document.body.appendChild(div);
+</script>
diff --git a/Source/WebCore/css/CSSSelector.h b/Source/WebCore/css/CSSSelector.h
index 2fd45f301465..ff02a2733aef 100644
--- a/Source/WebCore/css/CSSSelector.h
+++ b/Source/WebCore/css/CSSSelector.h
@@ -183,6 +183,10 @@ WTF_ALLOW_UNSAFE_BUFFER_USAGE_END
// Implicit means that this selector is not author/UA written.
bool isImplicit() const { return m_isImplicit; }
+#if !ASSERT_WITH_SECURITY_IMPLICATION_DISABLED
+ bool destructorHasBeenCalled() const { return m_destructorHasBeenCalled; }
+#endif
+
private:
friend class MutableCSSSelector;
diff --git a/Source/WebCore/css/CSSStyleSheet.cpp b/Source/WebCore/css/CSSStyleSheet.cpp
index 6d38ca370d6f..d1bb3ef9420c 100644
--- a/Source/WebCore/css/CSSStyleSheet.cpp
+++ b/Source/WebCore/css/CSSStyleSheet.cpp
@@ -230,6 +230,7 @@ void CSSStyleSheet::didMutateRules(RuleMutationType mutationType, ContentsCloned
{
ASSERT(m_contents->isMutable());
ASSERT(m_contents->hasOneClient());
+ m_contents->setHasResolvedNesting(false);
forEachStyleScope([&](Style::Scope& scope) {
if ((mutationType == RuleInsertion || mutationType == RuleReplace) && contentsClonedForMutation == ContentsClonedForMutation::No && !scope.activeStyleSheetsContains(*this)) {
diff --git a/Source/WebCore/css/SelectorChecker.cpp b/Source/WebCore/css/SelectorChecker.cpp
index 5ad7845ef9d4..5ce5b8b1cdf3 100644
--- a/Source/WebCore/css/SelectorChecker.cpp
+++ b/Source/WebCore/css/SelectorChecker.cpp
@@ -204,6 +204,8 @@ SelectorChecker::SelectorChecker(Document& document)
bool SelectorChecker::match(const CSSSelector& selector, const Element& element, CheckingContext& checkingContext) const
{
+ ASSERT_WITH_SECURITY_IMPLICATION(!selector.destructorHasBeenCalled());
+
auto pseudoElementIdentifier = checkingContext.pseudoId == PseudoId::None ? std::nullopt : std::optional(Style::PseudoElementIdentifier { checkingContext.pseudoId, checkingContext.pseudoElementNameArgument });
LocalContext context(selector, element, checkingContext.resolvingMode == SelectorChecker::Mode::QueryingRules ? VisitedMatchType::Disabled : VisitedMatchType::Enabled, pseudoElementIdentifier);
diff --git a/Source/WebCore/css/StyleSheetContents.h b/Source/WebCore/css/StyleSheetContents.h
index 461e4d32f608..8a6e79f99ceb 100644
--- a/Source/WebCore/css/StyleSheetContents.h
+++ b/Source/WebCore/css/StyleSheetContents.h
@@ -162,6 +162,9 @@ public:
friend class CSSStyleSheet;
+ bool hasResolvedNesting() const { return m_hasResolvedNesting; }
+ void setHasResolvedNesting(bool value) const { m_hasResolvedNesting = value; }
+
private:
WEBCORE_EXPORT StyleSheetContents(StyleRuleImport* ownerRule, const String& originalURL, const CSSParserContext&);
StyleSheetContents(const StyleSheetContents&);
@@ -187,6 +190,7 @@ private:
bool m_didLoadErrorOccur { false };
bool m_usesStyleBasedEditability { false };
bool m_isMutable { false };
+ mutable bool m_hasResolvedNesting { false };
mutable std::optional<bool> m_hasNestingRulesCache;
unsigned m_inMemoryCacheCount { 0 };
diff --git a/Source/WebCore/style/RuleSetBuilder.cpp b/Source/WebCore/style/RuleSetBuilder.cpp
index e6365a5d735e..ec89484f9e60 100644
--- a/Source/WebCore/style/RuleSetBuilder.cpp
+++ b/Source/WebCore/style/RuleSetBuilder.cpp
@@ -55,7 +55,7 @@ RuleSetBuilder::RuleSetBuilder(RuleSet& ruleSet, const MQ::MediaQueryEvaluator&
, m_mediaQueryCollector({ evaluator })
, m_resolver(resolver)
, m_shrinkToFit(shrinkToFit)
- , m_shouldResolveNesting(shouldResolveNesting)
+ , m_builderShouldResolveNesting(shouldResolveNesting)
{
}
@@ -136,10 +136,10 @@ void RuleSetBuilder::addChildRule(Ref<StyleRuleBase> rule)
// https://drafts.csswg.org/css-nesting/#nesting-at-scope
// For the purposes of the style rules in its body and its own <scope-end> selector,
// the @scope rule is treated as an ancestor style rule, matching the elements matched by its <scope-start> selector.
- if (m_shouldResolveNesting == ShouldResolveNesting::Yes) {
+ if (m_shouldResolveNestingForSheet) {
const CSSSelectorList* parentResolvedSelectorList = nullptr;
if (m_selectorListStack.size())
- parentResolvedSelectorList = m_selectorListStack.last();
+ parentResolvedSelectorList = m_selectorListStack.last();
if (!scopeRule->originalScopeStart().isEmpty())
scopeRule->setScopeStart(CSSSelectorParser::resolveNestingParent(scopeRule->originalScopeStart(), parentResolvedSelectorList));
if (!scopeRule->originalScopeEnd().isEmpty())
@@ -246,6 +246,8 @@ void RuleSetBuilder::addChildRule(Ref<StyleRuleBase> rule)
void RuleSetBuilder::addRulesFromSheetContents(const StyleSheetContents& sheet)
{
+ auto nestingResolveScope = SetForScope { m_shouldResolveNestingForSheet, m_builderShouldResolveNesting == ShouldResolveNesting::Yes && !sheet.hasResolvedNesting() };
+
for (auto& rule : sheet.layerRulesBeforeImportRules())
registerLayers(rule->nameList());
@@ -272,10 +274,15 @@ void RuleSetBuilder::addRulesFromSheetContents(const StyleSheetContents& sheet)
}
addChildRules(sheet.childRules());
+
+ if (m_shouldResolveNestingForSheet)
+ sheet.setHasResolvedNesting(true);
}
void RuleSetBuilder::resolveSelectorListWithNesting(StyleRuleWithNesting& rule)
{
+ ASSERT(m_shouldResolveNestingForSheet);
+
const CSSSelectorList* parentResolvedSelectorList = nullptr;
if (m_selectorListStack.size())
parentResolvedSelectorList = m_selectorListStack.last();
@@ -303,7 +310,7 @@ void RuleSetBuilder::addStyleRuleWithSelectorList(const CSSSelectorList& selecto
void RuleSetBuilder::addStyleRule(StyleRuleWithNesting& rule)
{
- if (m_shouldResolveNesting == ShouldResolveNesting::Yes)
+ if (m_shouldResolveNestingForSheet)
resolveSelectorListWithNesting(rule);
const auto& selectorList = rule.selectorList();
diff --git a/Source/WebCore/style/RuleSetBuilder.h b/Source/WebCore/style/RuleSetBuilder.h
index 66bba6e91485..09aa9395b8a3 100644
--- a/Source/WebCore/style/RuleSetBuilder.h
+++ b/Source/WebCore/style/RuleSetBuilder.h
@@ -89,7 +89,8 @@ private:
RuleSet::CascadeLayerIdentifier m_currentCascadeLayerIdentifier { 0 };
Vector<const CSSSelectorList*> m_selectorListStack;
Vector<CSSParserEnum::NestedContextType> m_ancestorStack;
- const ShouldResolveNesting m_shouldResolveNesting { ShouldResolveNesting::No };
+ const ShouldResolveNesting m_builderShouldResolveNesting { ShouldResolveNesting::No };
+ bool m_shouldResolveNestingForSheet { false };
RuleSet::ContainerQueryIdentifier m_currentContainerQueryIdentifier { 0 };
RuleSet::ScopeRuleIdentifier m_currentScopeIdentifier { 0 };
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: Gary Kwong, and an anonymous researcher
Key insight. The JIT direct-eval operations read VM from the caller frame but never set the VM’s top call frame via the tracer, so an allocation/throw during eval walked a garbage frame; installing JITOperationPrologueCallFrameTracer fixes the top-frame invariant.
Patch Details
diff --git a/JSTests/stress/eval-call-should-update-top-frame.js b/JSTests/stress/eval-call-should-update-top-frame.js
new file mode 100644
index 000000000000..711d3696244b
--- /dev/null
+++ b/JSTests/stress/eval-call-should-update-top-frame.js
@@ -0,0 +1,7 @@
+for (let i = 0; i < 10000; ++i) {
+ const v23 = (2 ** 31) - 1;
+ const v28 = (-1).toLocaleString(2, 31).padEnd(v23, "aa");
+ try {
+ eval(v28);
+ } catch { }
+}
diff --git a/Source/JavaScriptCore/jit/JITOperations.cpp b/Source/JavaScriptCore/jit/JITOperations.cpp
index 5f72bf90c87c..c82b4c8784f7 100644
--- a/Source/JavaScriptCore/jit/JITOperations.cpp
+++ b/Source/JavaScriptCore/jit/JITOperations.cpp
@@ -2387,8 +2387,10 @@ JSC_DEFINE_JIT_OPERATION(operationPutByValSetPrivateFieldGeneric, void, (JSGloba
JSC_DEFINE_JIT_OPERATION(operationCallDirectEvalSloppy, EncodedJSValue, (void* frame, JSScope* callerScopeChain, EncodedJSValue encodedThisValue))
{
CallFrame* calleeFrame = reinterpret_cast<CallFrame*>(frame);
+ CallFrame* callFrame = calleeFrame->callerFrame();
// We can't trust our callee since it could be garbage but our caller's should be ok.
- VM& vm = calleeFrame->callerFrame()->deprecatedVM();
+ VM& vm = callFrame->deprecatedVM();
+ JITOperationPrologueCallFrameTracer tracer(vm, callFrame);
auto scope = DECLARE_THROW_SCOPE(vm);
calleeFrame->setCodeBlock(nullptr);
@@ -2398,8 +2400,10 @@ JSC_DEFINE_JIT_OPERATION(operationCallDirectEvalSloppy, EncodedJSValue, (void* f
JSC_DEFINE_JIT_OPERATION(operationCallDirectEvalStrict, EncodedJSValue, (void* frame, JSScope* callerScopeChain, EncodedJSValue encodedThisValue))
{
CallFrame* calleeFrame = reinterpret_cast<CallFrame*>(frame);
+ CallFrame* callFrame = calleeFrame->callerFrame();
// We can't trust our callee since it could be garbage but our caller's should be ok.
- VM& vm = calleeFrame->callerFrame()->deprecatedVM();
+ VM& vm = callFrame->deprecatedVM();
+ JITOperationPrologueCallFrameTracer tracer(vm, callFrame);
auto scope = DECLARE_THROW_SCOPE(vm);
calleeFrame->setCodeBlock(nullptr);
@@ -2409,8 +2413,10 @@ JSC_DEFINE_JIT_OPERATION(operationCallDirectEvalStrict, EncodedJSValue, (void* f
JSC_DEFINE_JIT_OPERATION(operationCallDirectEvalSloppyTaintedByWithScope, EncodedJSValue, (void* frame, JSScope* callerScopeChain, EncodedJSValue encodedThisValue))
{
CallFrame* calleeFrame = reinterpret_cast<CallFrame*>(frame);
+ CallFrame* callFrame = calleeFrame->callerFrame();
// We can't trust our callee since it could be garbage but our caller's should be ok.
- VM& vm = calleeFrame->callerFrame()->deprecatedVM();
+ VM& vm = callFrame->deprecatedVM();
+ JITOperationPrologueCallFrameTracer tracer(vm, callFrame);
auto scope = DECLARE_THROW_SCOPE(vm);
calleeFrame->setCodeBlock(nullptr);
@@ -2420,8 +2426,10 @@ JSC_DEFINE_JIT_OPERATION(operationCallDirectEvalSloppyTaintedByWithScope, Encode
JSC_DEFINE_JIT_OPERATION(operationCallDirectEvalStrictTaintedByWithScope, EncodedJSValue, (void* frame, JSScope* callerScopeChain, EncodedJSValue encodedThisValue))
{
CallFrame* calleeFrame = reinterpret_cast<CallFrame*>(frame);
+ CallFrame* callFrame = calleeFrame->callerFrame();
// We can't trust our callee since it could be garbage but our caller's should be ok.
- VM& vm = calleeFrame->callerFrame()->deprecatedVM();
+ VM& vm = callFrame->deprecatedVM();
+ JITOperationPrologueCallFrameTracer tracer(vm, callFrame);
auto scope = DECLARE_THROW_SCOPE(vm);
calleeFrame->setCodeBlock(nullptr);
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: A malicious website may be able to track users in Safari private browsing mode
Researchers: an anonymous researcher
Key insight. ClearKey session IDs came from a process-global static counter, so the script-visible sessionId persisted and incremented across instances and contexts; scoping the counter per CDM instance removes the cross-context tracking signal.
Patch Details
diff --git a/LayoutTests/http/tests/media/clearkey/clear-key-session-id-expected.txt b/LayoutTests/http/tests/media/clearkey/clear-key-session-id-expected.txt
new file mode 100644
index 000000000000..bcf6b799c2a9
--- /dev/null
+++ b/LayoutTests/http/tests/media/clearkey/clear-key-session-id-expected.txt
@@ -0,0 +1,11 @@
+Test that ClearKey session identifiers do not persist across MediaKeySystem instances.
+-
+Create the first pair of sessions
+EXPECTED (session1.sessionId == '0') OK
+EXPECTED (session2.sessionId == '1') OK
+-
+Create the second pair of sessions
+EXPECTED (session1.sessionId == '0') OK
+EXPECTED (session2.sessionId == '1') OK
+END OF TEST
+
diff --git a/LayoutTests/http/tests/media/clearkey/clear-key-session-id.html b/LayoutTests/http/tests/media/clearkey/clear-key-session-id.html
new file mode 100644
index 000000000000..46982acafb80
--- /dev/null
+++ b/LayoutTests/http/tests/media/clearkey/clear-key-session-id.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>clear-key-session-id</title>
+ <script src=../../media-resources/video-test.js></script>
+ <script src="support.js"></script>
+ <script>
+
+ var session1;
+ var session2;
+
+ async function runTest() {
+ let createTwoSessions = async () => {
+ const config = [{ "initDataTypes":["webm"], "audioCapabilities":[{"contentType":"audio/mp4"}]}];
+ let mediaKeySystem = await navigator.requestMediaKeySystemAccess("org.w3.clearkey", config);
+ let mediaKeys = await mediaKeySystem.createMediaKeys();
+
+ const e = new Uint8Array([0]);
+
+ session1 = await mediaKeys.createSession();
+ let request = await session1.generateRequest("webm", e);
+
+ testExpected('session1.sessionId', '0');
+
+ session2 = await mediaKeys.createSession();
+ request = await session2.generateRequest("webm", e);
+
+ testExpected('session2.sessionId', '1');
+ };
+
+ consoleWrite('-');
+ consoleWrite('Create the first pair of sessions');
+ await createTwoSessions();
+
+ consoleWrite('-');
+ consoleWrite('Create the second pair of sessions');
+ await createTwoSessions();
+ }
+
+ window.addEventListener('load', event => {
+ runTest().then(endTest).catch(failTest);
+ });
+ </script>
+</head>
+<body>
+ <div>Test that ClearKey session identifiers do not persist across MediaKeySystem instances.</div>
+</body>
+</html>
\ No newline at end of file
diff --git a/Source/WebCore/platform/encryptedmedia/clearkey/CDMClearKey.cpp b/Source/WebCore/platform/encryptedmedia/clearkey/CDMClearKey.cpp
index 45892247809f..78f0e8faaca0 100644
--- a/Source/WebCore/platform/encryptedmedia/clearkey/CDMClearKey.cpp
+++ b/Source/WebCore/platform/encryptedmedia/clearkey/CDMClearKey.cpp
@@ -448,10 +448,10 @@ RefPtr<CDMInstanceSession> CDMInstanceClearKey::createSession()
void CDMInstanceSessionClearKey::requestLicense(LicenseType, KeyGroupingStrategy, const AtomString& initDataType, Ref<SharedBuffer>&& initData, LicenseCallback&& callback)
{
- static uint32_t s_sessionIdValue = 0;
- ++s_sessionIdValue;
-
- m_sessionID = String::number(s_sessionIdValue);
+ if (RefPtr parentInstance = protectedParentInstance())
+ m_sessionID = String::number(parentInstance->getNextSessionIdValue());
+ else
+ m_sessionID = emptyString();
if (equalLettersIgnoringASCIICase(initDataType, "cenc"_s))
initData = extractKeyidsFromCencInitData(initData.get());
diff --git a/Source/WebCore/platform/encryptedmedia/clearkey/CDMClearKey.h b/Source/WebCore/platform/encryptedmedia/clearkey/CDMClearKey.h
index c504566e502e..a1824f5fd161 100644
--- a/Source/WebCore/platform/encryptedmedia/clearkey/CDMClearKey.h
+++ b/Source/WebCore/platform/encryptedmedia/clearkey/CDMClearKey.h
@@ -96,6 +96,8 @@ public:
CDMInstanceClearKey();
virtual ~CDMInstanceClearKey();
+ uint32_t getNextSessionIdValue() { return m_nextSessionIdValue++; }
+
// CDMInstance
ImplementationType implementationType() const final { return ImplementationType::ClearKey; }
void initializeWithConfiguration(const CDMKeySystemConfiguration&, AllowDistinctiveIdentifiers, AllowPersistentState, SuccessCallback&&) final;
@@ -103,6 +105,9 @@ public:
void setStorageDirectory(const String&) final;
const String& keySystem() const final;
RefPtr<CDMInstanceSession> createSession() final;
+
+private:
+ uint32_t m_nextSessionIdValue { 0 };
};
class CDMInstanceSessionClearKey final : public CDMInstanceSessionProxy {
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: rheza (@ginggilBesel)
Key insight. Private-name field defines must not generate prototype property-miss conditions; the spurious conditions referenced structures that GC could free, leaving the cached case dangling.
Patch Details
diff --git a/JSTests/stress/static-private-fields-dont-need-property-conditions.js b/JSTests/stress/static-private-fields-dont-need-property-conditions.js
new file mode 100644
index 000000000000..37ccd62442c2
--- /dev/null
+++ b/JSTests/stress/static-private-fields-dont-need-property-conditions.js
@@ -0,0 +1,28 @@
+function f0() {
+}
+const v3 = Object.getOwnPropertyNames(this);
+this / Object;
+const v7 = f0();
+try {
+ Object(f0, true);
+} catch(e11) {
+}
+for (const v12 of v3) {
+ const v14 = this[v12];
+ for (let i16 = 0;
+ (() => {
+ class C21 extends Object {
+ static #f = v14;
+ 6 = Object;
+ }
+ new C21();
+ new C21();
+ new C21();
+ Object.__proto__ = v3;
+ new Float64Array(2887);
+ return i16 < 32;
+ })();
+ i16++) {
+ }
+ Object[v12] = this;
+}
\ No newline at end of file
diff --git a/Source/JavaScriptCore/bytecode/Repatch.cpp b/Source/JavaScriptCore/bytecode/Repatch.cpp
index 075b4de86dd3..0a69aac032c0 100644
--- a/Source/JavaScriptCore/bytecode/Repatch.cpp
+++ b/Source/JavaScriptCore/bytecode/Repatch.cpp
@@ -497,6 +497,9 @@ static InlineCacheAction tryCacheGetBy(JSGlobalObject* globalObject, CodeBlock*
newCase = AccessCase::create(vm, codeBlock, AccessCase::IndexedProxyObjectLoad, nullptr);
break;
}
+ case GetByKind::PrivateName:
+ case GetByKind::PrivateNameById:
+ RELEASE_ASSERT_NOT_REACHED();
default:
break;
}
@@ -635,6 +638,7 @@ static InlineCacheAction tryCacheGetBy(JSGlobalObject* globalObject, CodeBlock*
else {
if (isPrivate) {
RELEASE_ASSERT(!slot.isUnset());
+ RELEASE_ASSERT(conditionSet.isEmpty());
constexpr bool isGlobalProxy = false;
if (!slot.isCacheable())
return GiveUpOnCache;
@@ -1113,13 +1117,9 @@ static InlineCacheAction tryCachePutBy(JSGlobalObject* globalObject, CodeBlock*
break;
}
case PutByKind::DefinePrivateNameById:
- case PutByKind::DefinePrivateNameByVal: {
+ case PutByKind::DefinePrivateNameByVal:
ASSERT(ident.isPrivateName());
- conditionSet = generateConditionsForPropertyMiss(vm, codeBlock, globalObject, newStructure, ident.impl());
- if (!conditionSet.isValid())
- return GiveUpOnCache;
break;
- }
case PutByKind::ByIdDirectStrict:
case PutByKind::ByIdDirectSloppy:
case PutByKind::ByValDirectStrict:
@@ -1213,12 +1213,13 @@ static InlineCacheAction tryCachePutBy(JSGlobalObject* globalObject, CodeBlock*
}
case PutByKind::DefinePrivateNameById:
case PutByKind::DefinePrivateNameByVal:
+ case PutByKind::SetPrivateNameById:
+ case PutByKind::SetPrivateNameByVal:
+ RELEASE_ASSERT_NOT_REACHED();
case PutByKind::ByIdDirectStrict:
case PutByKind::ByIdDirectSloppy:
case PutByKind::ByValDirectStrict:
case PutByKind::ByValDirectSloppy:
- case PutByKind::SetPrivateNameById:
- case PutByKind::SetPrivateNameByVal:
return GiveUpOnCache;
}
}
@@ -1566,7 +1567,7 @@ static InlineCacheAction tryCacheInBy(
break;
}
default:
- break;
+ RELEASE_ASSERT_NOT_REACHED();
}
} else if (wasFound) {
if (!structure->propertyAccessesAreCacheable())
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Maliciously crafted web content may be able to break out of Web Content sandbox. This is a supplementary fix for an attack that was blocked in iOS 17.2. (Apple is aware of a report that this issue may have been exploited in an extremely sophisticated attack against specific targeted individuals on versions of iOS before iOS 17.2.)
Researchers: Apple
Key insight. WebGL enable/disable forwarded PRIMITIVE_RESTART_FIXED_INDEX — a capability the implementation must manage per WebGL version — straight to GL, letting content desynchronize primitive-restart state into an out-of-bounds condition; rejecting the toggle restores control to the implementation.
Overview
Impact:
Description: A maliciously crafted webpage may be able to fingerprint the user
Researchers: an anonymous researcher
Key insight. A missing ‘is this object actually rooted?’ precondition check (m_rootPath.isEmpty()) let two Entries-API operations (getParent, readEntries) touch and report real filesystem metadata for un-authorized paths; the file() path was already guarded, so this fix closes the sibling entry points that shared the same trust assumption but not the same check.
Overview
Impact:
Description: Copying a URL from Web Inspector may lead to command injection
Researchers: Johan Carlsson (joaxcar)
Key insight. The escaping was applied at the wrong layer: shell single-quoting protects against the shell, but the dangerous parsing happens inside curl, whose
--data-binary/--dataoptions interpret a leading ‘@’ as a filename after the shell strips the quotes — switching to--data-rawmoves the body to a sink that never performs that interpretation.
Overview
Impact:
Description: Processing web content may lead to a denial-of-service
Researchers: Q1IQ (@q1iqF) of NUS CuriOSity and P1umer (@p1umer) of Imperial Global Singapore
Key insight. BBQ-JIT bulk table/memory ops called allocate() on values that were already located, driving the register allocator into an invalid state and crashing; using loadIfNecessary() restores consistency.
Patch Details
diff --git a/JSTests/wasm/stress/array-copy-with-constant-null.js b/JSTests/wasm/stress/array-copy-with-constant-null.js
new file mode 100644
index 000000000000..dded10f50c95
--- /dev/null
+++ b/JSTests/wasm/stress/array-copy-with-constant-null.js
@@ -0,0 +1,57 @@
+import * as assert from '../assert.js'
+
+/*
+(module
+ (type $A (array (mut i32)))
+
+ (func (export "null_src") (result i32) (local (ref $A))
+ i32.const 4
+ i32.const 42
+ array.new $A
+ local.tee 0
+ i32.const 0
+ ref.null $A
+ i32.const 0
+ i32.const 4
+ array.copy $A $A
+
+ local.get 0
+ i32.const 0
+ array.get $A
+ )
+
+ (func (export "null_dst") (result i32) (local (ref $A))
+ ref.null $A
+ i32.const 0
+ i32.const 4
+ i32.const 42
+ array.new $A
+ local.tee 0
+ i32.const 0
+ i32.const 4
+ array.copy $A $A
+
+ local.get 0
+ i32.const 0
+ array.get $A
+ )
+)
+*/
+
+const wasm = new Uint8Array([
+ 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x88, 0x80, 0x80, 0x80, 0x00, 0x02, 0x5e, 0x7f,
+ 0x01, 0x60, 0x00, 0x01, 0x7f, 0x03, 0x83, 0x80, 0x80, 0x80, 0x00, 0x02, 0x01, 0x01, 0x07, 0x97, 0x80,
+ 0x80, 0x80, 0x00, 0x02, 0x08, 0x6e, 0x75, 0x6c, 0x6c, 0x5f, 0x73, 0x72, 0x63, 0x00, 0x00, 0x08, 0x6e,
+ 0x75, 0x6c, 0x6c, 0x5f, 0x64, 0x73, 0x74, 0x00, 0x01, 0x0a, 0xcd, 0x80, 0x80, 0x80, 0x00, 0x02, 0xa1,
+ 0x80, 0x80, 0x80, 0x00, 0x01, 0x01, 0x64, 0x00, 0x41, 0x04, 0x41, 0x2a, 0xfb, 0x06, 0x00, 0x22, 0x00,
+ 0x41, 0x00, 0xd0, 0x00, 0x41, 0x00, 0x41, 0x04, 0xfb, 0x11, 0x00, 0x00, 0x20, 0x00, 0x41, 0x00, 0xfb,
+ 0x0b, 0x00, 0x0b, 0xa1, 0x80, 0x80, 0x80, 0x00, 0x01, 0x01, 0x64, 0x00, 0xd0, 0x00, 0x41, 0x00, 0x41,
+ 0x04, 0x41, 0x2a, 0xfb, 0x06, 0x00, 0x22, 0x00, 0x41, 0x00, 0x41, 0x04, 0xfb, 0x11, 0x00, 0x00, 0x20,
+ 0x00, 0x41, 0x00, 0xfb, 0x0b, 0x00, 0x0b
+]);
+
+let instance = WebAssembly.instantiate(wasm);
+for (let i = 0; i < 50000; i ++) {
+ assert.throws(() => instance.exports.null_src(), TypeError);
+ assert.throws(() => instance.exports.null_dst(), TypeError);
+}
diff --git a/Source/JavaScriptCore/wasm/WasmBBQJIT.cpp b/Source/JavaScriptCore/wasm/WasmBBQJIT.cpp
index a583c0fc29fe..7eaa5e9f9bfe 100644
--- a/Source/JavaScriptCore/wasm/WasmBBQJIT.cpp
+++ b/Source/JavaScriptCore/wasm/WasmBBQJIT.cpp
@@ -830,7 +830,7 @@ PartialResult WARN_UNUSED_RETURN BBQJIT::addTableSet(unsigned tableIndex, Value
Value shouldThrow = topValue(TypeKind::I32);
emitCCall(&operationSetWasmTableElement, arguments, shouldThrow);
- Location shouldThrowLocation = allocate(shouldThrow);
+ Location shouldThrowLocation = loadIfNecessary(shouldThrow);
LOG_INSTRUCTION("TableSet", tableIndex, index, value);
@@ -857,7 +857,7 @@ PartialResult WARN_UNUSED_RETURN BBQJIT::addTableInit(unsigned elementIndex, uns
};
Value shouldThrow = topValue(TypeKind::I32);
emitCCall(&operationWasmTableInit, arguments, shouldThrow);
- Location shouldThrowLocation = allocate(shouldThrow);
+ Location shouldThrowLocation = loadIfNecessary(shouldThrow);
LOG_INSTRUCTION("TableInit", tableIndex, dstOffset, srcOffset, length);
@@ -921,7 +921,7 @@ PartialResult WARN_UNUSED_RETURN BBQJIT::addTableFill(unsigned tableIndex, Value
};
Value shouldThrow = topValue(TypeKind::I32);
emitCCall(&operationWasmTableFill, arguments, shouldThrow);
- Location shouldThrowLocation = allocate(shouldThrow);
+ Location shouldThrowLocation = loadIfNecessary(shouldThrow);
LOG_INSTRUCTION("TableFill", tableIndex, fill, offset, count);
@@ -946,7 +946,7 @@ PartialResult WARN_UNUSED_RETURN BBQJIT::addTableCopy(unsigned dstTableIndex, un
};
Value shouldThrow = topValue(TypeKind::I32);
emitCCall(&operationWasmTableCopy, arguments, shouldThrow);
- Location shouldThrowLocation = allocate(shouldThrow);
+ Location shouldThrowLocation = loadIfNecessary(shouldThrow);
LOG_INSTRUCTION("TableCopy", dstTableIndex, srcTableIndex, dstOffset, srcOffset, length);
@@ -1110,7 +1110,7 @@ PartialResult WARN_UNUSED_RETURN BBQJIT::addMemoryFill(Value dstAddress, Value t
};
Value shouldThrow = topValue(TypeKind::I32);
emitCCall(&operationWasmMemoryFill, arguments, shouldThrow);
- Location shouldThrowLocation = allocate(shouldThrow);
+ Location shouldThrowLocation = loadIfNecessary(shouldThrow);
throwExceptionIf(ExceptionType::OutOfBoundsMemoryAccess, m_jit.branchTest32(ResultCondition::Zero, shouldThrowLocation.asGPR()));
@@ -1133,7 +1133,7 @@ PartialResult WARN_UNUSED_RETURN BBQJIT::addMemoryCopy(Value dstAddress, Value s
};
Value shouldThrow = topValue(TypeKind::I32);
emitCCall(&operationWasmMemoryCopy, arguments, shouldThrow);
- Location shouldThrowLocation = allocate(shouldThrow);
+ Location shouldThrowLocation = loadIfNecessary(shouldThrow);
throwExceptionIf(ExceptionType::OutOfBoundsMemoryAccess, m_jit.branchTest32(ResultCondition::Zero, shouldThrowLocation.asGPR()));
@@ -1157,7 +1157,7 @@ PartialResult WARN_UNUSED_RETURN BBQJIT::addMemoryInit(unsigned dataSegmentIndex
};
Value shouldThrow = topValue(TypeKind::I32);
emitCCall(&operationWasmMemoryInit, arguments, shouldThrow);
- Location shouldThrowLocation = allocate(shouldThrow);
+ Location shouldThrowLocation = loadIfNecessary(shouldThrow);
throwExceptionIf(ExceptionType::OutOfBoundsMemoryAccess, m_jit.branchTest32(ResultCondition::Zero, shouldThrowLocation.asGPR()));
@@ -1262,7 +1262,7 @@ PartialResult WARN_UNUSED_RETURN BBQJIT::atomicWait(ExtAtomicOpType op, Expressi
emitCCall(&operationMemoryAtomicWait32, arguments, result);
else
emitCCall(&operationMemoryAtomicWait64, arguments, result);
- Location resultLocation = allocate(result);
+ Location resultLocation = loadIfNecessary(result);
LOG_INSTRUCTION(makeString(op), pointer, value, timeout, uoffset, RESULT(result));
@@ -1280,7 +1280,7 @@ PartialResult WARN_UNUSED_RETURN BBQJIT::atomicNotify(ExtAtomicOpType op, Expres
};
result = topValue(TypeKind::I32);
emitCCall(&operationMemoryAtomicNotify, arguments, result);
- Location resultLocation = allocate(result);
+ Location resultLocation = loadIfNecessary(result);
LOG_INSTRUCTION(makeString(op), pointer, count, uoffset, RESULT(result));
@@ -1439,7 +1439,7 @@ void BBQJIT::pushArrayNewFromSegment(ArraySegmentOperation operation, uint32_t t
};
result = topValue(TypeKind::I64);
emitCCall(operation, arguments, result);
- Location resultLocation = allocate(result);
+ Location resultLocation = loadIfNecessary(result);
emitThrowOnNullReference(exceptionType, resultLocation);
}
@@ -1463,6 +1463,14 @@ PartialResult WARN_UNUSED_RETURN BBQJIT::addArrayCopy(uint32_t dstTypeIndex, Exp
if (dst.isConst() || src.isConst()) {
ASSERT_IMPLIES(dst.isConst(), dst.asI64() == JSValue::encode(jsNull()));
ASSERT_IMPLIES(src.isConst(), src.asI64() == JSValue::encode(jsNull()));
+
+ LOG_INSTRUCTION("ArrayCopy", dstTypeIndex, dst, dstOffset, srcTypeIndex, src, srcOffset, size);
+
+ consume(dst);
+ consume(dstOffset);
+ consume(src);
+ consume(srcOffset);
+ consume(size);
emitThrowException(ExceptionType::NullArrayCopy);
return { };
}
@@ -1480,7 +1488,7 @@ PartialResult WARN_UNUSED_RETURN BBQJIT::addArrayCopy(uint32_t dstTypeIndex, Exp
};
Value shouldThrow = topValue(TypeKind::I32);
emitCCall(&operationWasmArrayCopy, arguments, shouldThrow);
- Location shouldThrowLocation = allocate(shouldThrow);
+ Location shouldThrowLocation = loadIfNecessary(shouldThrow);
LOG_INSTRUCTION("ArrayCopy", dstTypeIndex, dst, dstOffset, srcTypeIndex, src, srcOffset, size);
@@ -1495,6 +1503,12 @@ PartialResult WARN_UNUSED_RETURN BBQJIT::addArrayInitElem(uint32_t dstTypeIndex,
{
if (dst.isConst()) {
ASSERT(dst.asI64() == JSValue::encode(jsNull()));
+
+ LOG_INSTRUCTION("ArrayInitElem", dstTypeIndex, dst, dstOffset, srcElementIndex, srcOffset, size);
+
+ consume(dstOffset);
+ consume(srcOffset);
+ consume(size);
emitThrowException(ExceptionType::NullArrayInitElem);
return { };
}
@@ -1511,7 +1525,7 @@ PartialResult WARN_UNUSED_RETURN BBQJIT::addArrayInitElem(uint32_t dstTypeIndex,
};
Value shouldThrow = topValue(TypeKind::I32);
emitCCall(&operationWasmArrayInitElem, arguments, shouldThrow);
- Location shouldThrowLocation = allocate(shouldThrow);
+ Location shouldThrowLocation = loadIfNecessary(shouldThrow);
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: linjy of HKUS3Lab and chluo of WHUSecLab
Key insight. A tail call ends the frame permanently, so BBQ must drain (consume) the entire expression stack at that point; failing to release those register/slot bindings before the mandatory validation of the following unreachable code desynchronizes the allocator and crashes the JIT.
Patch Details
diff --git a/JSTests/wasm/stress/tail-call-should-consume-stack-in-bbq.js b/JSTests/wasm/stress/tail-call-should-consume-stack-in-bbq.js
new file mode 100644
index 000000000000..4a9d69e84d72
--- /dev/null
+++ b/JSTests/wasm/stress/tail-call-should-consume-stack-in-bbq.js
@@ -0,0 +1,151 @@
+/*
+(module
+ (tag $nonzero (param))
+ (tag $zero (param))
+
+ (type $i32-i32 (func (param i32) (result i32)))
+ (table $countdown-table funcref (elem $countdown-indirect))
+
+ (func $countdown (param i32) (result i32) (local f32)
+ f32.const 42
+ local.set 1
+
+ try
+ local.get 0
+ i32.const 0
+ i32.eq
+ if
+ throw $zero
+ else
+ throw $nonzero
+ end
+ catch $nonzero
+ local.get 0
+ local.get 0
+ i32.add
+
+ local.get 0
+ i32.const 1
+ i32.sub
+ return_call $countdown
+ catch_all
+ end
+
+ f32.const 0
+ local.get 1
+ f32.add
+ i32.trunc_f32_u
+ )
+
+ (func $countdown-indirect (param i32) (result i32) (local f32)
+ f32.const 42
+ local.set 1
+
+ try
+ local.get 0
+ i32.const 0
+ i32.eq
+ if
+ throw $zero
+ else
+ throw $nonzero
+ end
+ catch $nonzero
+ local.get 0
+ local.get 0
+ i32.add
+
+ local.get 0
+ i32.const 1
+ i32.sub
+ i32.const 0
+ return_call_indirect $countdown-table (type $i32-i32)
+ catch_all
+ end
+
+ f32.const 0
+ local.get 1
+ f32.add
+ i32.trunc_f32_u
+ )
+
+ (func $countdown-ref (param i32) (result i32) (local f32)
+ f32.const 42
+ local.set 1
+
+ try
+ local.get 0
+ i32.const 0
+ i32.eq
+ if
+ throw $zero
+ else
+ throw $nonzero
+ end
+ catch $nonzero
+ local.get 0
+ local.get 0
+ i32.add
+
+ local.get 0
+ i32.const 1
+ i32.sub
+ ref.func $countdown-ref
+ return_call_ref $i32-i32
+ catch_all
+ end
+
+ f32.const 0
+ local.get 1
+ f32.add
+ i32.trunc_f32_u
+ )
+
+ (func (export "main")
+ block
+ block
+ i32.const 100000
+ call $countdown
+ i32.const 42
+ i32.eq
+ br_if 0
+ unreachable
+ end
+ i32.const 100000
+ call $countdown-indirect
+ i32.const 42
+ i32.eq
+ br_if 0
+ unreachable
+ end
+ i32.const 100000
+ call $countdown-ref
+ i32.const 42
+ i32.eq
+ br_if 0
+ unreachable
+ )
+)
+*/
+
+var code = new Uint8Array([
+ 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x09, 0x02, 0x60, 0x01, 0x7f, 0x01, 0x7f, 0x60, 0x00, 0x00,
+ 0x03, 0x05, 0x04, 0x00, 0x00, 0x00, 0x01, 0x04, 0x05, 0x01, 0x70, 0x01, 0x01, 0x01, 0x0d, 0x05, 0x02, 0x00, 0x01,
+ 0x00, 0x01, 0x07, 0x08, 0x01, 0x04, 0x6d, 0x61, 0x69, 0x6e, 0x00, 0x03, 0x09, 0x0b, 0x02, 0x00, 0x41, 0x00, 0x0b,
+ 0x01, 0x01, 0x03, 0x00, 0x01, 0x02, 0x0a, 0xdb, 0x01, 0x04, 0x36, 0x01, 0x01, 0x7d, 0x43, 0x00, 0x00, 0x28, 0x42,
+ 0x21, 0x01, 0x06, 0x40, 0x20, 0x00, 0x41, 0x00, 0x46, 0x04, 0x40, 0x08, 0x01, 0x05, 0x08, 0x00, 0x0b, 0x00, 0x07,
+ 0x00, 0x20, 0x00, 0x20, 0x00, 0x6a, 0x1a, 0x20, 0x00, 0x41, 0x01, 0x6b, 0x12, 0x00, 0x19, 0x01, 0x0b, 0x43, 0x00,
+ 0x00, 0x00, 0x00, 0x20, 0x01, 0x92, 0xa9, 0x0b, 0x39, 0x01, 0x01, 0x7d, 0x43, 0x00, 0x00, 0x28, 0x42, 0x21, 0x01,
+ 0x06, 0x40, 0x20, 0x00, 0x41, 0x00, 0x46, 0x04, 0x40, 0x08, 0x01, 0x05, 0x08, 0x00, 0x0b, 0x00, 0x07, 0x00, 0x20,
+ 0x00, 0x20, 0x00, 0x6a, 0x1a, 0x20, 0x00, 0x41, 0x01, 0x6b, 0x41, 0x00, 0x13, 0x00, 0x00, 0x19, 0x01, 0x0b, 0x43,
+ 0x00, 0x00, 0x00, 0x00, 0x20, 0x01, 0x92, 0xa9, 0x0b, 0x38, 0x01, 0x01, 0x7d, 0x43, 0x00, 0x00, 0x28, 0x42, 0x21,
+ 0x01, 0x06, 0x40, 0x20, 0x00, 0x41, 0x00, 0x46, 0x04, 0x40, 0x08, 0x01, 0x05, 0x08, 0x00, 0x0b, 0x00, 0x07, 0x00,
+ 0x20, 0x00, 0x20, 0x00, 0x6a, 0x1a, 0x20, 0x00, 0x41, 0x01, 0x6b, 0xd2, 0x02, 0x15, 0x00, 0x19, 0x01, 0x0b, 0x43,
+ 0x00, 0x00, 0x00, 0x00, 0x20, 0x01, 0x92, 0xa9, 0x0b, 0x2f, 0x00, 0x02, 0x40, 0x02, 0x40, 0x02, 0x40, 0x41, 0xa0,
+ 0x8d, 0x06, 0x10, 0x00, 0x41, 0x2a, 0x46, 0x0d, 0x00, 0x00, 0x0b, 0x41, 0xa0, 0x8d, 0x06, 0x10, 0x01, 0x41, 0x2a,
+ 0x46, 0x0d, 0x00, 0x00, 0x0b, 0x41, 0xa0, 0x8d, 0x06, 0x10, 0x02, 0x41, 0x2a, 0x46, 0x0d, 0x00, 0x00, 0x0b, 0x0b
+]);
+
+var module = new WebAssembly.Module(code);
+var instance = new WebAssembly.Instance(module);
+instance.exports.main()
diff --git a/Source/JavaScriptCore/wasm/WasmBBQJIT.cpp b/Source/JavaScriptCore/wasm/WasmBBQJIT.cpp
index 7eaa5e9f9bfe..4b90d6952797 100644
--- a/Source/JavaScriptCore/wasm/WasmBBQJIT.cpp
+++ b/Source/JavaScriptCore/wasm/WasmBBQJIT.cpp
@@ -4092,7 +4092,6 @@ void BBQJIT::emitTailCall(unsigned functionIndex, const TypeDefinition& signatur
else
resolvedArguments.append(Value::pinned(arguments[i].type(), locationOf(arguments[i])));
- // This isn't really needed but it's nice to have good book keeping.
consume(arguments[i]);
}
@@ -4146,6 +4145,13 @@ void BBQJIT::emitTailCall(unsigned functionIndex, const TypeDefinition& signatur
}
LOG_INSTRUCTION("ReturnCall", functionIndex, arguments);
+
+ // Because we're returning, we need to consume all elements of the
+ // expression stack here so they don't spuriously hold onto their bindings
+ // in the subsequent unreachable code.
+
+ for (const auto& value : m_parser->expressionStack())
+ consume(value);
}
@@ -4338,6 +4344,13 @@ void BBQJIT::emitIndirectTailCall(const char* opcode, const Value& callee, GPRRe
m_jit.farJump(calleeCode, WasmEntryPtrTag);
LOG_INSTRUCTION(opcode, callee, arguments);
+
+ // Because we're returning, we need to consume all elements of the
+ // expression stack here so they don't spuriously hold onto their bindings
+ // in the subsequent unreachable code.
+
+ for (const auto& value : m_parser->expressionStack())
+ consume(value);
}
void BBQJIT::addRTTSlowPathJump(TypeIndex signature, GPRReg calleeRTT)
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to memory corruption
Researchers: an anonymous researcher
Key insight. The source bound for array.init_data was checked in element units while the copy ran in bytes, so size*elementSize could exceed the validated range; checking the byte extent with overflow detection closes the OOB.
Patch Details
diff --git a/JSTests/wasm/stress/array-init-data-bounds.js b/JSTests/wasm/stress/array-init-data-bounds.js
new file mode 100644
index 000000000000..07fc1612530b
--- /dev/null
+++ b/JSTests/wasm/stress/array-init-data-bounds.js
@@ -0,0 +1,30 @@
+/*
+(module
+ (memory (export "memory") 1)
+ (type $vec (array (mut i32)))
+ (data $data0 "1234")
+
+ (func $main(result (ref null $vec))
+ (local $arr(ref null $vec))
+ (local.set $arr (array.new_default $vec (i32.const 4)))
+
+ (array.init_data $vec $data0
+ (local.get $arr)
+ (i32.const 0)
+ (i32.const -2)
+ (i32.const 1))
+
+ (local.get $arr)
+ )
+
+ (export "main" (func $main))
+)
+ */
+var wasm_code = new Uint8Array([0x00,0x61,0x73,0x6d,0x01,0x00,0x00,0x00,0x01,0x89,0x80,0x80,0x80,0x00,0x02,0x5e,0x7f,0x01,0x60,0x00,0x01,0x63,0x00,0x03,0x82,0x80,0x80,0x80,0x00,0x01,0x01,0x05,0x83,0x80,0x80,0x80,0x00,0x01,0x00,0x01,0x07,0x91,0x80,0x80,0x80,0x00,0x02,0x06,0x6d,0x65,0x6d,0x6f,0x72,0x79,0x02,0x00,0x04,0x6d,0x61,0x69,0x6e,0x00,0x00,0x0c,0x81,0x80,0x80,0x80,0x00,0x01,0x0a,0xa0,0x80,0x80,0x80,0x00,0x01,0x9a,0x80,0x80,0x80,0x00,0x01,0x01,0x63,0x00,0x41,0x04,0xfb,0x07,0x00,0x21,0x00,0x20,0x00,0x41,0x00,0x41,0x7e,0x41,0x01,0xfb,0x12,0x00,0x00,0x20,0x00,0x0b,0x0b,0x87,0x80,0x80,0x80,0x00,0x01,0x01,0x04,0x31,0x32,0x33,0x34]);
+var wasm_module = new WebAssembly.Module(wasm_code);
+var wasm_instance = new WebAssembly.Instance(wasm_module);
+var f = wasm_instance.exports.main;
+try {
+ f();
+} catch {
+}
diff --git a/Source/JavaScriptCore/wasm/WasmOperationsInlines.h b/Source/JavaScriptCore/wasm/WasmOperationsInlines.h
index 757287d4bf3c..3d6816316672 100644
--- a/Source/JavaScriptCore/wasm/WasmOperationsInlines.h
+++ b/Source/JavaScriptCore/wasm/WasmOperationsInlines.h
@@ -440,13 +440,15 @@ inline bool arrayInitData(JSWebAssemblyInstance* instance, EncodedJSValue dst, u
if (lastDstElementIndexChecked > dstObject->size())
return false;
- CheckedUint32 lastSrcElementIndexChecked = srcOffset;
- lastSrcElementIndexChecked += size;
+ size_t elementSize = dstObject->elementType().type.elementSize();
- if (lastSrcElementIndexChecked.hasOverflowed())
+ CheckedUint32 lastSrcByteChecked = size;
+ lastSrcByteChecked *= elementSize;
+ lastSrcByteChecked += srcOffset;
+
+ if (lastSrcByteChecked.hasOverflowed())
return false;
- size_t elementSize = dstObject->elementType().type.elementSize();
return instance->copyDataSegment(dstObject, srcDataIndex, srcOffset, size * elementSize, dstObject->data() + dstOffset * elementSize);
}
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: Seunghyun Lee
Key insight. Mutable wasm globals and tables were type-checked covariantly at import instead of invariantly, letting two modules share the same mutable storage under incompatible reference types — a type confusion; requiring mutual subtyping fixes it.
Patch Details
diff --git a/JSTests/wasm/gc/linking.js b/JSTests/wasm/gc/linking.js
index 400aa6143af7..c8b8b82c269b 100644
--- a/JSTests/wasm/gc/linking.js
+++ b/JSTests/wasm/gc/linking.js
@@ -19,19 +19,6 @@ function testLinking() {
`, { m: { f1: m0.exports.f0 } });
}
- {
- const m0 = instantiate(`
- (module
- (type (func))
- (table (export "t0") 10 (ref null 0)))
- `);
-
- const m1 = instantiate(`
- (module
- (table (import "m" "t1") 10 (ref null func)))
- `, { m: { t1: m0.exports.t0 } });
- }
-
{
const m0 = instantiate(`
(module
@@ -44,19 +31,6 @@ function testLinking() {
(global (import "m" "g1") (ref null func)))
`, { m: { g1: m0.exports.g0 } });
}
-
- {
- const m0 = instantiate(`
- (module
- (type (func))
- (global (export "g0") (mut (ref null 0)) (ref.null 0)))
- `);
-
- const m1 = instantiate(`
- (module
- (global (import "m" "g1") (mut (ref null func))))
- `, { m: { g1: m0.exports.g0 } });
- }
}
testLinking();
diff --git a/Source/JavaScriptCore/wasm/js/WebAssemblyModuleRecord.cpp b/Source/JavaScriptCore/wasm/js/WebAssemblyModuleRecord.cpp
index 53b13a78c1b5..39eb6ab37af4 100644
--- a/Source/JavaScriptCore/wasm/js/WebAssemblyModuleRecord.cpp
+++ b/Source/JavaScriptCore/wasm/js/WebAssemblyModuleRecord.cpp
@@ -361,7 +361,7 @@ void WebAssemblyModuleRecord::initializeImports(JSGlobalObject* globalObject, JS
if (!value.inherits<JSWebAssemblyGlobal>())
return exception(createJSWebAssemblyLinkError(globalObject, vm, importFailMessage(import, "imported global"_s, "must be a WebAssembly.Global object since it is mutable"_s)));
JSWebAssemblyGlobal* globalValue = jsCast<JSWebAssemblyGlobal*>(value);
- if (!isSubtype(globalValue->global()->type(), global.type))
+ if (!isSubtype(globalValue->global()->type(), global.type) || !isSubtype(global.type, globalValue->global()->type()))
return exception(createJSWebAssemblyLinkError(globalObject, vm, importFailMessage(import, "imported global"_s, "must be a same type"_s)));
if (globalValue->global()->mutability() != global.mutability)
return exception(createJSWebAssemblyLinkError(globalObject, vm, importFailMessage(import, "imported global"_s, "must be a same mutability"_s)));
@@ -393,7 +393,7 @@ void WebAssemblyModuleRecord::initializeImports(JSGlobalObject* globalObject, JS
auto expectedType = moduleInformation.tables[import.kindIndex].wasmType();
auto actualType = table->table()->wasmType();
- if (!Wasm::isSubtype(actualType, expectedType))
+ if (!Wasm::isSubtype(actualType, expectedType) || !Wasm::isSubtype(expectedType, actualType))
return exception(createJSWebAssemblyLinkError(globalObject, vm, importFailMessage(import, "Table import"_s, "provided a 'type' that is wrong"_s)));
// ii. Append v to tables.
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: Brendon Tiszka of Google Project Zero
Key insight. DocumentFontLoader had no lifetime protection, so script running during loadPendingFonts() could destroy the document and free the loader mid-iteration; ref-counting it through its Document and protecting
thisfixes the UAF.
Patch Details
diff --git a/LayoutTests/http/tests/security/document-cached-font-loading-expected.txt b/LayoutTests/http/tests/security/document-cached-font-loading-expected.txt
new file mode 100644
index 000000000000..654ddf7f17ef
--- /dev/null
+++ b/LayoutTests/http/tests/security/document-cached-font-loading-expected.txt
@@ -0,0 +1 @@
+This test passes if it does not crash.
diff --git a/LayoutTests/http/tests/security/document-cached-font-loading.html b/LayoutTests/http/tests/security/document-cached-font-loading.html
new file mode 100644
index 000000000000..38a0812044ea
--- /dev/null
+++ b/LayoutTests/http/tests/security/document-cached-font-loading.html
@@ -0,0 +1,22 @@
+<head>
+<script>
+if (window.testRunner) {
+ testRunner.dumpAsText();
+}
+</script>
+<style>
+/* cache the font */
+@font-face {
+ font-family: 'uaf';
+ src: url(a.ttf);
+}
+body {
+ font-family: 'uaf';
+}
+</style>
+</head>
+<body>
+ This test passes if it does not crash.
+ <iframe id="a" onload="document.getElementById('a').remove();" src="../resources/document-cached-font-loading-helper.html"></iframe>
+ <iframe id="b" onload="document.getElementById('b').remove();" src="../resources/document-cached-font-loading-helper.html"></iframe>
+</body>
diff --git a/LayoutTests/http/tests/security/resources/document-cached-font-loading-helper.html b/LayoutTests/http/tests/security/resources/document-cached-font-loading-helper.html
new file mode 100644
index 000000000000..a5fdf917f5eb
--- /dev/null
+++ b/LayoutTests/http/tests/security/resources/document-cached-font-loading-helper.html
@@ -0,0 +1,12 @@
+<head>
+<style>
+@font-face {
+ font-family: 'uaf';
+ src: url(font.ttf);
+}
+body {
+ font-family: 'uaf';
+}
+</style>
+</head>
+<body>text</body>
diff --git a/LayoutTests/http/tests/security/resources/font.ttf b/LayoutTests/http/tests/security/resources/font.ttf
new file mode 100644
index 000000000000..672bf761fe9e
Binary files /dev/null and b/LayoutTests/http/tests/security/resources/font.ttf differ
diff --git a/Source/WebCore/dom/Document.cpp b/Source/WebCore/dom/Document.cpp
index 994791cc207b..086344234d18 100644
--- a/Source/WebCore/dom/Document.cpp
+++ b/Source/WebCore/dom/Document.cpp
@@ -1017,7 +1017,7 @@ DocumentFontLoader& Document::ensureFontLoader()
{
ASSERT(m_constructionDidFinish);
ASSERT(!m_fontLoader);
- m_fontLoader = makeUnique<DocumentFontLoader>(*this);
+ m_fontLoader = makeUniqueWithoutRefCountedCheck<DocumentFontLoader>(*this);
return *m_fontLoader;
}
diff --git a/Source/WebCore/dom/DocumentFontLoader.cpp b/Source/WebCore/dom/DocumentFontLoader.cpp
index be8629ab5969..929caa15eef6 100644
--- a/Source/WebCore/dom/DocumentFontLoader.cpp
+++ b/Source/WebCore/dom/DocumentFontLoader.cpp
@@ -99,6 +99,7 @@ void DocumentFontLoader::loadPendingFonts()
void DocumentFontLoader::fontLoadingTimerFired()
{
+ Ref protectedThis { *this };
loadPendingFonts();
// FIXME: Use SubresourceLoader instead.
diff --git a/Source/WebCore/dom/DocumentFontLoader.h b/Source/WebCore/dom/DocumentFontLoader.h
index 7dba4f427ff6..dee268564819 100644
--- a/Source/WebCore/dom/DocumentFontLoader.h
+++ b/Source/WebCore/dom/DocumentFontLoader.h
@@ -43,6 +43,9 @@ public:
DocumentFontLoader(Document&);
~DocumentFontLoader();
+ void ref() const { m_document->ref(); }
+ void deref() const { m_document->deref(); }
+
CachedFont* cachedFont(URL&&, bool, bool, LoadedFromOpaqueSource);
void beginLoadingFontSoon(CachedFont&);
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to memory corruption
Researchers: Gary Kwong
Key insight. ToString/CallStringConstructor unconditionally marked their operand as UsesAsOther even when it could not be an ‘other’ type, feeding inconsistent usage flags into representation choices; gating the flag on SpecOther fixes the miscompilation.
Patch Details
diff --git a/JSTests/stress/string-add-conversion-unused.js b/JSTests/stress/string-add-conversion-unused.js
new file mode 100644
index 000000000000..96e1377859a0
--- /dev/null
+++ b/JSTests/stress/string-add-conversion-unused.js
@@ -0,0 +1,14 @@
+var x = [, 9999999999];
+var count = 0;
+for (
+ y = 10;
+ (function () {
+ z = y;
+ " " + x[y];
+ return 9;
+ })();
+ y--
+) {
+ if (count >= 99) break;
+ count++;
+}
diff --git a/Source/JavaScriptCore/dfg/DFGBackwardsPropagationPhase.cpp b/Source/JavaScriptCore/dfg/DFGBackwardsPropagationPhase.cpp
index d1c2540344cd..e2eb6b334eaf 100644
--- a/Source/JavaScriptCore/dfg/DFGBackwardsPropagationPhase.cpp
+++ b/Source/JavaScriptCore/dfg/DFGBackwardsPropagationPhase.cpp
@@ -528,7 +528,9 @@ private:
case ToString:
case CallStringConstructor: {
- node->child1()->mergeFlags(NodeBytecodeUsesAsNumber | NodeBytecodeUsesAsOther | NodeBytecodeNeedsNaNOrInfinity);
+ if (typeFilterFor(node->child1().useKind()) & SpecOther)
+ node->child1()->mergeFlags(NodeBytecodeUsesAsOther);
+ node->child1()->mergeFlags(NodeBytecodeUsesAsNumber | NodeBytecodeNeedsNaNOrInfinity);
break;
}
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: Xiangwei Zhang of Tencent Security YUNDING LAB, linjy of HKUS3Lab and chluo of WHUSecLab, and an anonymous researcher
Key insight. BBQ-JIT catch handlers didn’t clear register bindings on entry the way the unreachable-end path did, so stale bindings miscompiled the catch; unifying on unbindAllRegisters() restores a consistent register model.
Patch Details
diff --git a/JSTests/wasm/stress/compile-unreachable-catch.js b/JSTests/wasm/stress/compile-unreachable-catch.js
new file mode 100644
index 000000000000..c509346e9189
--- /dev/null
+++ b/JSTests/wasm/stress/compile-unreachable-catch.js
@@ -0,0 +1,77 @@
+import { instantiate } from "../wabt-wrapper.js";
+import * as assert from "../assert.js";
+
+const watGetGlobalsSameTempReturnCall = `
+(module
+ (tag $e (param f32))
+ (global $f0 (mut f32)(f32.const 2.0))
+ (global $i0 (mut i64)(i64.const 1))
+ (func (export "test") (local i64)
+ try
+ local.get 0
+ global.get $f0
+ return_call $bar
+ catch_all
+ global.get $i0
+ return_call $foo
+ end
+ )
+ (func $foo)
+ (func $bar)
+)
+`;
+
+const watGetGlobalCatchArgSameTempReturnCall = `
+(module
+ (tag $e (param i64))
+ (global $f0 (mut f32)(f32.const 2.0))
+ (global $i0 (mut i64)(i64.const 1))
+ (func (export "test") (result i64)
+ try (result i64)
+ global.get $f0
+ global.get $f0
+ return_call $bar
+ catch $e
+ global.get $i0
+ i64.add
+ end
+ )
+ (func $bar (param f32) (result i64) i64.const 3)
+)
+`;
+
+const watGetGlobalsSameTempUnreachable = `
+(module
+ (tag $e (param f32))
+ (global $f0 (mut f32)(f32.const 2.0))
+ (global $i0 (mut i64)(i64.const 1))
+ (func (export "test") (local i64)
+ try
+ local.get 0
+ global.get $f0
+ unreachable
+ catch_all
+ global.get $i0
+ return_call $foo
+ end
+ )
+ (func $foo)
+ (func $bar)
+)
+`;
+
+async function runOne(wat, isUnreachable) {
+ const instance = await instantiate(wat, {}, { exceptions: true, tail_call: true });
+ const {test} = instance.exports;
+ if (isUnreachable) {
+ assert.throws(() => {
+ test();
+ }, WebAssembly.RuntimeError, `Unreachable code`);
+ } else {
+ test();
+ }
+}
+
+assert.asyncTest(runOne(watGetGlobalsSameTempReturnCall, false));
+assert.asyncTest(runOne(watGetGlobalCatchArgSameTempReturnCall, false));
+assert.asyncTest(runOne(watGetGlobalsSameTempUnreachable, true));
\ No newline at end of file
diff --git a/Source/JavaScriptCore/wasm/WasmBBQJIT.cpp b/Source/JavaScriptCore/wasm/WasmBBQJIT.cpp
index 092590b9bdec..1e8c7e32a6f1 100644
--- a/Source/JavaScriptCore/wasm/WasmBBQJIT.cpp
+++ b/Source/JavaScriptCore/wasm/WasmBBQJIT.cpp
@@ -3463,6 +3463,7 @@ PartialResult WARN_UNUSED_RETURN BBQJIT::addCatch(unsigned exceptionIndex, const
PartialResult WARN_UNUSED_RETURN BBQJIT::addCatchToUnreachable(unsigned exceptionIndex, const TypeDefinition& exceptionSignature, ControlType& data, ResultList& results)
{
m_usesExceptions = true;
+ unbindAllRegisters();
ControlData dataCatch(*this, BlockType::Catch, data.signature(), data.enclosedHeight());
dataCatch.setCatchKind(CatchKind::Catch);
if (ControlData::isTry(data)) {
@@ -3507,6 +3508,7 @@ PartialResult WARN_UNUSED_RETURN BBQJIT::addCatchAll(Stack& expressionStack, Con
PartialResult WARN_UNUSED_RETURN BBQJIT::addCatchAllToUnreachable(ControlType& data)
{
m_usesExceptions = true;
+ unbindAllRegisters();
ControlData dataCatch(*this, BlockType::Catch, data.signature(), data.enclosedHeight());
dataCatch.setCatchKind(CatchKind::CatchAll);
if (ControlData::isTry(data)) {
@@ -3749,14 +3751,7 @@ PartialResult WARN_UNUSED_RETURN BBQJIT::addEndToUnreachable(ControlEntry& entry
Type type = entryData.signature()->returnType(i);
entry.enclosedExpressionStack.constructAndAppend(type, Value::fromTemp(type.kind, entryData.enclosedHeight() + entryData.implicitSlots() + i));
}
- for (const auto& binding : m_gprBindings) {
- if (!binding.isNone())
- consume(binding.toValue());
- }
- for (const auto& binding : m_fprBindings) {
- if (!binding.isNone())
- consume(binding.toValue());
- }
+ unbindAllRegisters();
} else {
unsigned offset = stack.size() - returnCount;
for (unsigned i = 0; i < returnCount; ++i)
@@ -4872,6 +4867,18 @@ void BBQJIT::unbind(Value value, Location loc)
dataLogLn("BBQ\tUnbound value ", value, " from ", loc);
}
+void BBQJIT::unbindAllRegisters()
+{
+ for (const auto& binding : m_gprBindings) {
+ if (!binding.isNone())
+ consume(binding.toValue());
+ }
+ for (const auto& binding : m_fprBindings) {
+ if (!binding.isNone())
+ consume(binding.toValue());
+ }
+}
+
GPRReg BBQJIT::nextGPR()
{
auto next = m_gprSet.begin();
diff --git a/Source/JavaScriptCore/wasm/WasmBBQJIT.h b/Source/JavaScriptCore/wasm/WasmBBQJIT.h
index f33f42b4053e..ad1ced3e5c1d 100644
--- a/Source/JavaScriptCore/wasm/WasmBBQJIT.h
+++ b/Source/JavaScriptCore/wasm/WasmBBQJIT.h
@@ -1955,6 +1955,8 @@ private:
void unbind(Value value, Location loc);
+ void unbindAllRegisters();
+
template<typename Register>
static Register fromJSCReg(Reg reg)
{
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to memory corruption
Researchers: Tashita Software Security
Key insight. Storing DOM Nodes as raw non-owning pointers in the touch/wheel EventTargetSet made memory safety depend on every teardown path perfectly pruning the set; the fix removes that fragile obligation by making the container hold auto-nulling weak references, turning a latent use-after-free into a structurally impossible one.
Patch Details
diff --git a/Source/WTF/wtf/WeakHashCountedSet.h b/Source/WTF/wtf/WeakHashCountedSet.h
index a4af95bf87b5..6aa449841455 100644
--- a/Source/WTF/wtf/WeakHashCountedSet.h
+++ b/Source/WTF/wtf/WeakHashCountedSet.h
@@ -64,6 +64,11 @@ public:
bool remove(const ValueType&);
bool remove(iterator);
+ // Removes the value, regardless of its count.
+ // Returns true if a value was removed.
+ bool removeAll(const ValueType&);
+ bool removeAll(iterator);
+
// Clears the whole set.
void clear() { m_impl.clear(); }
@@ -111,6 +116,22 @@ inline bool WeakHashCountedSet<Value, WeakPtrImpl>::remove(iterator it)
return true;
}
+template<typename Value, typename WeakPtrImpl>
+inline bool WeakHashCountedSet<Value, WeakPtrImpl>::removeAll(const ValueType& value)
+{
+ return removeAll(find(value));
+}
+
+template<typename Value, typename WeakPtrImpl>
+inline bool WeakHashCountedSet<Value, WeakPtrImpl>::removeAll(iterator it)
+{
+ if (it == end())
+ return false;
+
+ m_impl.remove(it);
+ return true;
+}
+
template<typename Value, typename WeakMapImpl>
size_t containerSize(const WeakHashCountedSet<Value, WeakMapImpl>& container) { return container.computeSize(); }
diff --git a/Source/WebCore/dom/Document.cpp b/Source/WebCore/dom/Document.cpp
index 81729be58ab5..4e901c60f3d1 100644
--- a/Source/WebCore/dom/Document.cpp
+++ b/Source/WebCore/dom/Document.cpp
@@ -3232,11 +3232,11 @@ void Document::willBeRemovedFromFrame()
commonTeardown();
#if ENABLE(TOUCH_EVENTS)
- if (m_touchEventTargets && m_touchEventTargets->size() && parentDocument())
+ if (m_touchEventTargets && m_touchEventTargets->computeSize() && parentDocument())
protectedParentDocument()->didRemoveEventTargetNode(*this);
#endif
- if (m_wheelEventTargets && m_wheelEventTargets->size() && parentDocument())
+ if (m_wheelEventTargets && m_wheelEventTargets->computeSize() && parentDocument())
protectedParentDocument()->didRemoveEventTargetNode(*this);
if (RefPtr mediaQueryMatcher = m_mediaQueryMatcher)
@@ -8363,7 +8363,7 @@ void Document::wheelEventHandlersChanged(Node* node)
UNUSED_PARAM(node);
#endif
- bool haveHandlers = m_wheelEventTargets && !m_wheelEventTargets->isEmpty();
+ bool haveHandlers = m_wheelEventTargets && !m_wheelEventTargets->isEmptyIgnoringNullReferences();
page->chrome().client().wheelEventHandlersChanged(haveHandlers);
}
@@ -8372,7 +8372,7 @@ void Document::didAddWheelEventHandler(Node& node)
if (!m_wheelEventTargets)
m_wheelEventTargets = makeUnique<EventTargetSet>();
- m_wheelEventTargets->add(&node);
+ m_wheelEventTargets->add(node);
wheelEventHandlersChanged(&node);
if (RefPtr frame = this->frame())
@@ -8383,9 +8383,9 @@ static bool removeHandlerFromSet(EventTargetSet& handlerSet, Node& node, EventHa
{
switch (removal) {
case EventHandlerRemoval::One:
- return handlerSet.remove(&node);
+ return handlerSet.remove(node);
case EventHandlerRemoval::All:
- return handlerSet.removeAll(&node);
+ return handlerSet.removeAll(node);
}
return false;
}
@@ -8410,7 +8410,7 @@ unsigned Document::wheelEventHandlerCount() const
return 0;
unsigned count = 0;
- for (auto& handler : *m_wheelEventTargets)
+ for (auto handler : *m_wheelEventTargets)
count += handler.value;
return count;
@@ -8422,7 +8422,7 @@ void Document::didAddTouchEventHandler(Node& handler)
if (!m_touchEventTargets)
m_touchEventTargets = makeUnique<EventTargetSet>();
- m_touchEventTargets->add(&handler);
+ m_touchEventTargets->add(handler);
if (RefPtr parent = parentDocument()) {
parent->didAddTouchEventHandler(*this);
@@ -8453,15 +8453,15 @@ void Document::didRemoveEventTargetNode(Node& handler)
{
#if ENABLE(TOUCH_EVENTS)
if (m_touchEventTargets) {
- m_touchEventTargets->removeAll(&handler);
- if ((&handler == this || m_touchEventTargets->isEmpty()) && parentDocument())
+ m_touchEventTargets->removeAll(handler);
+ if ((&handler == this || m_touchEventTargets->isEmptyIgnoringNullReferences()) && parentDocument())
protectedParentDocument()->didRemoveEventTargetNode(*this);
}
#endif
if (m_wheelEventTargets) {
- m_wheelEventTargets->removeAll(&handler);
- if ((&handler == this || m_wheelEventTargets->isEmpty()) && parentDocument())
+ m_wheelEventTargets->removeAll(handler);
+ if ((&handler == this || m_wheelEventTargets->isEmptyIgnoringNullReferences()) && parentDocument())
protectedParentDocument()->didRemoveEventTargetNode(*this);
}
}
@@ -8473,7 +8473,7 @@ unsigned Document::touchEventHandlerCount() const
return 0;
unsigned count = 0;
- for (auto& handler : *m_touchEventTargets)
+ for (auto handler : *m_touchEventTargets)
count += handler.value;
return count;
@@ -8545,12 +8545,11 @@ Document::RegionFixedPair Document::absoluteRegionForEventTargets(const EventTar
Region targetRegion;
bool insideFixedPosition = false;
- for (auto& keyValuePair : *targets) {
- if (RefPtr node = keyValuePair.key) {
- auto targetRegionFixedPair = absoluteEventRegionForNode(*node);
- targetRegion.unite(targetRegionFixedPair.first);
- insideFixedPosition |= targetRegionFixedPair.second;
- }
+ for (auto keyValuePair : *targets) {
+ Ref node = keyValuePair.key;
+ auto targetRegionFixedPair = absoluteEventRegionForNode(node);
+ targetRegion.unite(targetRegionFixedPair.first);
+ insideFixedPosition |= targetRegionFixedPair.second;
}
return RegionFixedPair(targetRegion, insideFixedPosition);
diff --git a/Source/WebCore/dom/Document.h b/Source/WebCore/dom/Document.h
index d4874fbbf3a3..3fe0bb244379 100644
--- a/Source/WebCore/dom/Document.h
+++ b/Source/WebCore/dom/Document.h
@@ -56,7 +56,6 @@
#include <wtf/Deque.h>
#include <wtf/FixedVector.h>
#include <wtf/Forward.h>
-#include <wtf/HashCountedSet.h>
#include <wtf/HashSet.h>
#include <wtf/Logger.h>
#include <wtf/ObjectIdentifier.h>
@@ -64,6 +63,7 @@
#include <wtf/RobinHoodHashMap.h>
#include <wtf/TriState.h>
#include <wtf/UniqueRef.h>
+#include <wtf/WeakHashCountedSet.h>
#include <wtf/WeakHashMap.h>
#include <wtf/WeakHashSet.h>
#include <wtf/WeakListHashSet.h>
@@ -356,7 +356,7 @@ enum class NodeListInvalidationType : uint8_t {
const auto numNodeListInvalidationTypes = enumToUnderlyingType(NodeListInvalidationType::InvalidateOnAnyAttrChange) + 1;
enum class EventHandlerRemoval : bool { One, All };
-using EventTargetSet = HashCountedSet<Node*>;
+using EventTargetSet = WeakHashCountedSet<Node, WeakPtrImplWithEventTargetData>;
enum class DocumentCompatibilityMode : uint8_t {
NoQuirksMode = 1,
@@ -1481,8 +1481,8 @@ public:
WEBCORE_EXPORT unsigned styleRecalcCount() const;
#if ENABLE(TOUCH_EVENTS)
- bool hasTouchEventHandlers() const { return m_touchEventTargets.get() ? m_touchEventTargets->size() : false; }
- bool touchEventTargetsContain(Node& node) const { return m_touchEventTargets ? m_touchEventTargets->contains(&node) : false; }
+ bool hasTouchEventHandlers() const { return m_touchEventTargets && m_touchEventTargets->computeSize(); }
+ bool touchEventTargetsContain(Node& node) const { return m_touchEventTargets && m_touchEventTargets->contains(node); }
#else
bool hasTouchEventHandlers() const { return false; }
bool touchEventTargetsContain(Node&) const { return false; }
@@ -1516,7 +1516,7 @@ public:
#endif
}
- bool hasWheelEventHandlers() const { return m_wheelEventTargets.get() ? m_wheelEventTargets->size() : false; }
+ bool hasWheelEventHandlers() const { return m_wheelEventTargets && m_wheelEventTargets->computeSize(); }
const EventTargetSet* wheelEventTargets() const { return m_wheelEventTargets.get(); }
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to memory corruption
Researchers: Lukas Bernhard, Gary Kwong, and an anonymous researcher
Key insight. Conflating out-of-bounds sane-chain accesses with in-bounds accesses in CSE’s HeapLocation model let an OOB PutByVal be rewritten as a bounds-check-free PutByValAlias; giving OOB accesses distinct location kinds restores the invariant that PutByValAlias is only ever emitted for genuinely in-bounds stores.
Patch Details
diff --git a/JSTests/stress/put-by-val-alias-out-of-bounds.js b/JSTests/stress/put-by-val-alias-out-of-bounds.js
new file mode 100644
index 000000000000..ebd74a75ae6d
--- /dev/null
+++ b/JSTests/stress/put-by-val-alias-out-of-bounds.js
@@ -0,0 +1,9 @@
+let array = new Uint32Array(65535);
+function test() {
+ for (let i = 0; i < 1e5; i += 6)
+ array[i] &= 2;
+ return array;
+}
+noInline(test);
+
+test();
\ No newline at end of file
diff --git a/Source/JavaScriptCore/dfg/DFGCSEPhase.cpp b/Source/JavaScriptCore/dfg/DFGCSEPhase.cpp
index 1909d7741fed..1c044e4bbf2c 100644
--- a/Source/JavaScriptCore/dfg/DFGCSEPhase.cpp
+++ b/Source/JavaScriptCore/dfg/DFGCSEPhase.cpp
@@ -524,6 +524,7 @@ private:
bool run(BasicBlock* block)
{
+ dataLogLnIf(DFGCSEPhaseInternal::verbose, "Starting block: ", block->index);
m_maps.clear();
m_changed = false;
m_block = block;
@@ -607,11 +608,13 @@ private:
void write(AbstractHeap heap)
{
+ dataLogLnIf(DFGCSEPhaseInternal::verbose, "\tWrite to heap ", heap);
m_maps.write(heap);
}
void def(PureValue value)
{
+ dataLogLnIf(DFGCSEPhaseInternal::verbose, "\tDef of value ", value, " at node ", m_node->index());
Node* match = m_maps.addPure(value, m_node);
if (!match)
return;
@@ -622,6 +625,7 @@ private:
void def(const HeapLocation& location, const LazyNode& value)
{
+ dataLogLnIf(DFGCSEPhaseInternal::verbose, "\tDef to ", location, " of value ", value, " at node ", m_node->index());
LazyNode match = m_maps.addImpure(location, value);
if (!match)
return;
diff --git a/Source/JavaScriptCore/dfg/DFGClobberize.h b/Source/JavaScriptCore/dfg/DFGClobberize.h
index 2fc89697731f..592a668ba9ee 100644
--- a/Source/JavaScriptCore/dfg/DFGClobberize.h
+++ b/Source/JavaScriptCore/dfg/DFGClobberize.h
@@ -60,14 +60,6 @@ void clobberize(Graph& graph, Node* node, const ReadFunctor& read, const WriteFu
// easiest way of indicating that something cannot be hoisted is to claim
// that it side-effects some miscellaneous thing.
//
- // - We cannot hoist forward-exiting nodes without some additional effort. I
- // believe that what it comes down to is that forward-exiting generally have
- // their NodeExitsForward cleared upon hoist, except for forward-exiting
- // nodes that take bogus state as their input. Those are substantially
- // harder. We disable it for now. In the future we could enable it by having
- // versions of those nodes that backward-exit instead, but I'm not convinced
- // of the soundness.
- //
// - Some nodes lie, and claim that they do not read the JSCell_structureID,
// JSCell_typeInfoFlags, etc. These are nodes that use the structure in a way
// that does not depend on things that change under structure transitions.
@@ -1116,8 +1108,11 @@ void clobberize(Graph& graph, Node* node, const ReadFunctor& read, const WriteFu
if (mode.mayBeResizableOrGrowableSharedTypedArray()) {
write(MiscFields);
write(TypedArrayProperties);
- } else
+ } else {
+ if (mode.isOutOfBounds())
+ indexedPropertyLoc = indexedPropertyLocToOutOfBoundsSaneChain(indexedPropertyLoc);
def(HeapLocation(indexedPropertyLoc, TypedArrayProperties, graph.varArgChild(node, 0), graph.varArgChild(node, 1)), LazyNode(node));
+ }
return;
// We should not get an AnyTypedArray in a GetByVal as AnyTypedArray is only created from intrinsics, which
// are only added from Inline Caching a GetById.
@@ -1167,7 +1162,7 @@ void clobberize(Graph& graph, Node* node, const ReadFunctor& read, const WriteFu
return;
case Array::Int32:
- if (node->arrayMode().isOutOfBounds()) {
+ if (mode.isOutOfBounds()) {
clobberTop();
return;
}
@@ -1175,14 +1170,14 @@ void clobberize(Graph& graph, Node* node, const ReadFunctor& read, const WriteFu
read(Butterfly_vectorLength);
read(IndexedInt32Properties);
write(IndexedInt32Properties);
- if (node->arrayMode().mayStoreToHole())
+ if (mode.mayStoreToHole())
write(Butterfly_publicLength);
def(HeapLocation(indexedPropertyLoc, IndexedInt32Properties, base, index), LazyNode(value));
def(HeapLocation(IndexedPropertyInt32OutOfBoundsSaneChainLoc, IndexedInt32Properties, base, index), LazyNode(value));
return;
case Array::Double:
- if (node->arrayMode().isOutOfBounds()) {
+ if (mode.isOutOfBounds()) {
clobberTop();
return;
}
@@ -1190,7 +1185,7 @@ void clobberize(Graph& graph, Node* node, const ReadFunctor& read, const WriteFu
read(Butterfly_vectorLength);
read(IndexedDoubleProperties);
write(IndexedDoubleProperties);
- if (node->arrayMode().mayStoreToHole())
+ if (mode.mayStoreToHole())
write(Butterfly_publicLength);
def(HeapLocation(IndexedPropertyDoubleLoc, IndexedDoubleProperties, base, index), LazyNode(value));
def(HeapLocation(IndexedPropertyDoubleSaneChainLoc, IndexedDoubleProperties, base, index), LazyNode(value));
@@ -1198,7 +1193,7 @@ void clobberize(Graph& graph, Node* node, const ReadFunctor& read, const WriteFu
return;
case Array::Contiguous:
- if (node->arrayMode().isOutOfBounds()) {
+ if (mode.isOutOfBounds()) {
clobberTop();
return;
}
@@ -1206,7 +1201,7 @@ void clobberize(Graph& graph, Node* node, const ReadFunctor& read, const WriteFu
read(Butterfly_vectorLength);
read(IndexedContiguousProperties);
write(IndexedContiguousProperties);
- if (node->arrayMode().mayStoreToHole())
+ if (mode.mayStoreToHole())
write(Butterfly_publicLength);
def(HeapLocation(indexedPropertyLoc, IndexedContiguousProperties, base, index), LazyNode(value));
def(HeapLocation(IndexedPropertyJSOutOfBoundsSaneChainLoc, IndexedContiguousProperties, base, index), LazyNode(value));
@@ -1221,12 +1216,12 @@ void clobberize(Graph& graph, Node* node, const ReadFunctor& read, const WriteFu
read(Butterfly_vectorLength);
read(IndexedArrayStorageProperties);
write(IndexedArrayStorageProperties);
- if (node->arrayMode().mayStoreToHole())
+ if (mode.mayStoreToHole())
write(Butterfly_publicLength);
return;
case Array::SlowPutArrayStorage:
- if (node->arrayMode().mayStoreToHole()) {
+ if (mode.mayStoreToHole()) {
clobberTop();
return;
}
@@ -1246,7 +1241,7 @@ void clobberize(Graph& graph, Node* node, const ReadFunctor& read, const WriteFu
case Array::Float16Array:
case Array::Float32Array:
case Array::Float64Array:
- if (node->arrayMode().mayBeResizableOrGrowableSharedTypedArray()) {
+ if (mode.mayBeResizableOrGrowableSharedTypedArray()) {
read(TypedArrayProperties);
read(MiscFields);
write(TypedArrayProperties);
@@ -2266,7 +2261,7 @@ void clobberize(Graph& graph, Node* node, const ReadFunctor& read, const WriteFu
write(MiscFields);
write(TypedArrayProperties);
} else {
- LocationKind indexedPropertyLoc = indexedPropertyLocForResultType(node->result());
+ LocationKind indexedPropertyLoc = indexedPropertyLocToOutOfBoundsSaneChain(indexedPropertyLocForResultType(node->result()));
def(HeapLocation(indexedPropertyLoc, AbstractHeap(TypedArrayProperties, node->dataViewData().asQuadWord), node->child1(), node->child2(), node->child3()), LazyNode(node));
}
return;
diff --git a/Source/JavaScriptCore/dfg/DFGHeapLocation.cpp b/Source/JavaScriptCore/dfg/DFGHeapLocation.cpp
index 3675e7845f7b..21cc6315d8da 100644
--- a/Source/JavaScriptCore/dfg/DFGHeapLocation.cpp
+++ b/Source/JavaScriptCore/dfg/DFGHeapLocation.cpp
@@ -47,254 +47,5 @@ void HeapLocation::dump(PrintStream& out) const
} } // namespace JSC::DFG
-namespace WTF {
-
-using namespace JSC::DFG;
-
-void printInternal(PrintStream& out, LocationKind kind)
-{
- switch (kind) {
- case InvalidLocationKind:
- out.print("InvalidLocationKind");
- return;
-
- case InvalidationPointLoc:
- out.print("InvalidationPointLoc");
- return;
-
- case TypeOfIsObjectLoc:
- out.print("TypeOfIsObjectLoc");
- return;
-
- case TypeOfIsFunctionLoc:
- out.print("TypeOfIsFunctionLoc");
- return;
-
- case IsCallableLoc:
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to arbitrary code execution. Apple is aware of a report that this issue may have been actively exploited on Intel-based Mac systems.
Researchers: Clément Lecigne and Benoît Sevens of Google’s Threat Analysis Group
Key insight. All scratch registers a JIT path needs must be reserved before operand setup; reserving scratch2 after getIntTypedArrayStoreOperand let it alias a live register and mis-compile the store.
Overview
Impact:
Description: Processing maliciously crafted web content may lead to a cross site scripting attack. Apple is aware of a report that this issue may have been actively exploited on Intel-based Mac systems.
Researchers: Clément Lecigne and Benoît Sevens of Google’s Threat Analysis Group
Key insight. Cookie first-party access was a binary allow-or-terminate decision that could not cleanly deny a cross-site cookie request; a tri-state Allow/Disallow/Terminate lets the NetworkProcess refuse disallowed cookie access without leaking it or crashing – closing an in-the-wild XSS vector.
Overview
Impact:
Description: Cookies belonging to one origin may be sent to another origin
Researchers: Wojciech Regula of SecuRing (
Key insight. Classifying a web-archive load by the raw URL string (which includes query/fragment/encoding) instead of the filesystem path let an attacker hide or fake the .webarchive suffix, bypassing the ephemeral-store isolation and leaking cookies across origins.
Patch Details
diff --git a/Source/WebKit/UIProcess/WebPageProxy.cpp b/Source/WebKit/UIProcess/WebPageProxy.cpp
index a36d0ca506c9..fd0ead8b1a10 100644
--- a/Source/WebKit/UIProcess/WebPageProxy.cpp
+++ b/Source/WebKit/UIProcess/WebPageProxy.cpp
@@ -4674,7 +4674,7 @@ void WebPageProxy::receivedNavigationActionPolicyDecision(WebProcessProxy& proce
if (preferences().loadWebArchiveWithEphemeralStorageEnabled() && navigation && policyAction == PolicyAction::Use && navigationAction->navigationType() != NavigationType::Reload && !isPolicyDataStore) {
bool isSubstituteWebArchive = navigation->substituteData() && MIMETypeRegistry::isWebArchiveMIMEType(navigation->substituteData()->MIMEType);
auto webarchiveURL = isSubstituteWebArchive ? URL { navigation->substituteData()->baseURL } : navigation->currentRequest().url();
- if (isSubstituteWebArchive || (webarchiveURL.protocolIsFile() && webarchiveURL.string().endsWith(".webarchive"_s))) {
+ if (isSubstituteWebArchive || (webarchiveURL.protocolIsFile() && webarchiveURL.fileSystemPath().endsWith(".webarchive"_s))) {
WEBPAGEPROXY_RELEASE_LOG(Loading, "receivedNavigationActionPolicyDecision: Swapping in non-persistent websiteDataStore for web archive.");
if (navigation->targetItem() && navigation->targetItem()->dataStoreForWebArchive())
websiteDataStore = *navigation->targetItem()->dataStoreForWebArchive();
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: an anonymous researcher, Q1IQ (@q1iqF) and P1umer (@p1umer)
Key insight. Two set/coalesce routines in YARR character-class construction implicitly required sorted match vectors, but ‘v’-flag string members with case-folding could append code points out of order, so the missing sort broke a merge invariant and led to a crash — an unenforced ordering precondition on data an attacker can shape.
Overview
Impact:
Description: Processing maliciously crafted web content may prevent Content Security Policy from being enforced
Researchers: Narendra Bhati, Manager of Cyber Security at Suma Soft Pvt. Ltd, Pune (India)
Key insight. A request type (hyperlink auditing pings) was omitted from CSP connect-src enforcement, so the fix is simply to gate sendPing() through allowConnectToSource; the root cause is an incomplete enumeration of CSP-checked connection sinks rather than any flaw in CSP itself.
Overview
Impact:
Description: Processing maliciously crafted web content may lead to universal cross site scripting
Researchers: Ron Masas
Key insight. The root cause is that web-archive content — which can spoof any origin and carry script — was loaded against the user’s persistent data store with normal permission and first-party-cookie treatment; the fix isolates every archive load into a throwaway non-persistent WebsiteDataStore (threaded through provisional pages, back/forward items, and cookie bookkeeping) and hard-denies permission queries once an archive is loaded.
Overview
Impact:
Description: Visiting a malicious website may lead to address bar spoofing
Researchers: Hafiizh and YoKo Kho (@yokoacc) of HakTrak
Background
Advisory: Apple Security Advisory
| Attribute | Value |
|---|---|
| CVE | CVE-2024-40866 |
| Bugzilla | 279451 |
| Component | WebKit |
| Bug Class | LogicError |
| Severity | medium |
| Advisory | Apple Advisory |
Fix not public. The WebKit fix for this bug is embargoed or not yet disclosed. The bug ID is confirmed from the Apple advisory, but no public commit references it.
Overview
Impact:
Description: A malicious website may exfiltrate data cross-origin
Researchers: Narendra Bhati, Manager of Cyber Security at Suma Soft Pvt. Ltd, Pune (India)
Background
Advisory: Apple Security Advisory
| Attribute | Value |
|---|---|
| CVE | CVE-2024-44187 |
| Bugzilla | 279452 |
| Component | WebKit |
| Bug Class | CrossOrigin |
| Severity | medium |
| Advisory | Apple Advisory |
Fix not public. The WebKit fix for this bug is embargoed or not yet disclosed. The bug ID is confirmed from the Apple advisory, but no public commit references it.
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: Tashita Software Security
Key insight. The code encoded a structural assumption (’the RubyBlock’s immediate first child is the anonymous ruby box’) as an ASSERT rather than a real check, and render-tree continuations violate that assumption, so in release builds the bad downcast/misparenting corrupts the render tree; the fix turns the assumption into an actual search for the anonymous box.
Overview
Impact:
Description: A malicious website may exfiltrate data cross-origin
Researchers: Narendra Bhati, Manager of Cyber Security At Suma Soft Pvt. Ltd, Pune (India)
Background
Advisory: Apple Security Advisory
| Attribute | Value |
|---|---|
| CVE | CVE-2024-54467 |
| Bugzilla | 287874 |
| Component | WebKit |
| Bug Class | CrossOrigin |
| Severity | medium |
| Advisory | Apple Advisory |
Fix not public. The WebKit fix for this bug is embargoed or not yet disclosed. The bug ID is confirmed from the Apple advisory, but no public commit references it.
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: Huang Xilin of Ant Group Light-Year Security Lab
Key insight. The resampler borrowed the caller’s transient buffer as its AudioBus channel-0 memory instead of owning it, so concurrent AudioWorklet processing could leave the bus pointing at stale/wrong-sized memory; owning all channel memory removes the UAF/OOB.
Patch Details
diff --git a/LayoutTests/webaudio/crashtest/audioworklet-concurrent-resampler-crash-expected.txt b/LayoutTests/webaudio/crashtest/audioworklet-concurrent-resampler-crash-expected.txt
new file mode 100644
index 000000000000..654ddf7f17ef
--- /dev/null
+++ b/LayoutTests/webaudio/crashtest/audioworklet-concurrent-resampler-crash-expected.txt
@@ -0,0 +1 @@
+This test passes if it does not crash.
diff --git a/LayoutTests/webaudio/crashtest/audioworklet-concurrent-resampler-crash.html b/LayoutTests/webaudio/crashtest/audioworklet-concurrent-resampler-crash.html
new file mode 100644
index 000000000000..b3ab181d4787
--- /dev/null
+++ b/LayoutTests/webaudio/crashtest/audioworklet-concurrent-resampler-crash.html
@@ -0,0 +1,44 @@
+<html>
+<head>
+ <script>
+ let worklet_source = `
+ class Processor extends AudioWorkletProcessor {
+ process(inputs, outputs, parameters) {
+ return true;
+ }
+ }
+ registerProcessor('P2', Processor);
+ `;
+
+ let blob = new Blob([worklet_source], { type: 'application/javascript' });
+ let worklet = URL.createObjectURL(blob);
+
+ var ctx = new AudioContext({ sampleRate: 44100});
+ const dest = ctx.destination;
+ dest.channelCountMode = "max";
+
+ async function main() {
+ await ctx.audioWorklet.addModule(worklet);
+ var script_processor = ctx.createScriptProcessor();
+ script_processor.onaudioprocess = function() {
+ dest.channelCount = 1;
+ audio_worklet.disconnect();
+ if (window.testRunner)
+ testRunner.notifyDone();
+ }
+ var audio_worklet = new AudioWorkletNode(ctx, "P2");
+ script_processor.connect(audio_worklet);
+ audio_worklet.connect(dest);
+ }
+ </script>
+</head>
+<body onload="main()">
+ <p>This test passes if it does not crash.</p>
+ <script>
+ if (window.testRunner) {
+ testRunner.waitUntilDone();
+ testRunner.dumpAsText();
+ }
+ </script>
+</body>
+</html>
diff --git a/Source/WebCore/platform/audio/MultiChannelResampler.cpp b/Source/WebCore/platform/audio/MultiChannelResampler.cpp
index e5a0cfc10caa..c44df274cbbc 100644
--- a/Source/WebCore/platform/audio/MultiChannelResampler.cpp
+++ b/Source/WebCore/platform/audio/MultiChannelResampler.cpp
@@ -42,19 +42,8 @@ namespace WebCore {
MultiChannelResampler::MultiChannelResampler(double scaleFactor, unsigned numberOfChannels, unsigned requestFrames, Function<void(AudioBus*, size_t framesToProcess)>&& provideInput)
: m_numberOfChannels(numberOfChannels)
, m_provideInput(WTFMove(provideInput))
- , m_multiChannelBus(AudioBus::create(numberOfChannels, requestFrames, false))
+ , m_multiChannelBus(AudioBus::create(numberOfChannels, requestFrames))
{
- // As an optimization, we will use the buffer passed to provideInputForChannel() as channel memory for the first channel so we
- // only need to allocate memory if there is more than one channel.
- if (numberOfChannels > 1) {
- m_channelsMemory = Vector<std::unique_ptr<AudioFloatArray>>(numberOfChannels - 1, [&](size_t i) {
- size_t channelIndex = i + 1;
- auto floatArray = makeUnique<AudioFloatArray>(requestFrames);
- m_multiChannelBus->setChannelMemory(channelIndex, floatArray->data(), requestFrames);
- return floatArray;
- });
- }
-
// Create each channel's resampler.
m_kernels = Vector<std::unique_ptr<SincResampler>>(numberOfChannels, [&](size_t channelIndex) {
return makeUnique<SincResampler>(scaleFactor, requestFrames, std::bind(&MultiChannelResampler::provideInputForChannel, this, std::placeholders::_1, std::placeholders::_2, channelIndex));
@@ -93,16 +82,10 @@ void MultiChannelResampler::process(AudioBus* destination, size_t framesToProces
void MultiChannelResampler::provideInputForChannel(std::span<float> buffer, size_t framesToProcess, unsigned channelIndex)
{
ASSERT(channelIndex < m_multiChannelBus->numberOfChannels());
- ASSERT(framesToProcess == m_multiChannelBus->length());
+ ASSERT(framesToProcess <= m_multiChannelBus->length());
- if (!channelIndex) {
- // As an optimization, we use the provided buffer as memory for the first channel in the AudioBus. This avoids
- // having to memcpy() for the first channel.
- RELEASE_ASSERT(framesToProcess <= buffer.size());
- m_multiChannelBus->setChannelMemory(0, buffer.data(), framesToProcess);
+ if (!channelIndex)
m_provideInput(m_multiChannelBus.get(), framesToProcess);
- return;
- }
// Copy the channel data from what we received from m_multiChannelProvider.
memcpySpan(buffer.subspan(0, framesToProcess), m_multiChannelBus->channel(channelIndex)->span().subspan(0, framesToProcess));
diff --git a/Source/WebCore/platform/audio/MultiChannelResampler.h b/Source/WebCore/platform/audio/MultiChannelResampler.h
index 25d43100b71f..214ee06567ac 100644
--- a/Source/WebCore/platform/audio/MultiChannelResampler.h
+++ b/Source/WebCore/platform/audio/MultiChannelResampler.h
@@ -29,7 +29,6 @@
#ifndef MultiChannelResampler_h
#define MultiChannelResampler_h
-#include "AudioArray.h"
#include <memory>
#include <wtf/Function.h>
#include <wtf/Vector.h>
@@ -62,7 +61,6 @@ private:
size_t m_outputFramesReady { 0 };
Function<void(AudioBus*, size_t framesToProcess)> m_provideInput;
RefPtr<AudioBus> m_multiChannelBus;
- Vector<std::unique_ptr<AudioFloatArray>> m_channelsMemory;
};
} // namespace WebCore
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: Huang Xilin of Ant Group Light-Year Security Lab
Key insight. The interpolation branch enforced its buffer-boundary invariant only partially (it wrapped readIndex2 but not the general case), so reverse playback at a fractional start offset could still index past the buffer; the fix is a defensive per-iteration bounds check on both interpolation indices before dereferencing the source.
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: Huang Xilin of Ant Group Light-Year Security Lab
Key insight. A degenerate audio configuration — an extreme negative detune that rounds the effective playback rate to exactly zero — routes rendering into a special-case branch that copied a single source sample without validating that its index stayed within the buffer, an unchecked-index invariant that the surrounding non-zero-rate branches enforce but this one did not.
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: Maksymilian Motyl
Key insight. Ruby render-tree building assumed a RubyBlock’s first child is the Ruby box, but continuations insert anonymous wrappers, so it returned the wrong renderer; locating the child by its actual Ruby display type fixes it.
Patch Details
diff --git a/LayoutTests/fast/ruby/ruby-block-continuation-crash-expected.txt b/LayoutTests/fast/ruby/ruby-block-continuation-crash-expected.txt
new file mode 100644
index 000000000000..f85a15505104
--- /dev/null
+++ b/LayoutTests/fast/ruby/ruby-block-continuation-crash-expected.txt
@@ -0,0 +1,3 @@
+base with
+forced
+line break annotation This test passes if it doesn't crash.
diff --git a/LayoutTests/fast/ruby/ruby-block-continuation-crash.html b/LayoutTests/fast/ruby/ruby-block-continuation-crash.html
new file mode 100644
index 000000000000..3f762d4236ea
--- /dev/null
+++ b/LayoutTests/fast/ruby/ruby-block-continuation-crash.html
@@ -0,0 +1,9 @@
+<script>
+if (window.testRunner)
+ testRunner.dumpAsText();
+</script>
+<ruby style="position: absolute">
+ <rb><span>base with <div>forced</div> line break</span></rb>
+ <rt>annotation</rt>
+</ruby>
+This test passes if it doesn't crash.
diff --git a/Source/WebCore/rendering/updating/RenderTreeBuilderRuby.cpp b/Source/WebCore/rendering/updating/RenderTreeBuilderRuby.cpp
index 9259b2330dd4..fde1f46e67d7 100644
--- a/Source/WebCore/rendering/updating/RenderTreeBuilderRuby.cpp
+++ b/Source/WebCore/rendering/updating/RenderTreeBuilderRuby.cpp
@@ -65,10 +65,17 @@ RenderElement& RenderTreeBuilder::Ruby::findOrCreateParentForStyleBasedRubyChild
if (!child.isRenderText() && child.style().display() == DisplayType::Ruby && parent.style().display() == DisplayType::RubyBlock)
return parent;
- if (parent.style().display() == DisplayType::RubyBlock && parent.firstChild()) {
+ if (parent.style().display() == DisplayType::RubyBlock) {
// See if we have an anonymous ruby box already.
- ASSERT(parent.firstChild()->style().display() == DisplayType::Ruby);
- return downcast<RenderElement>(*parent.firstChild());
+ // FIXME: It should be the immediate child but continuations can break this assumption.
+ for (CheckedPtr first = parent.firstChild(); first; first = first->firstChildSlow()) {
+ if (!first->isAnonymous()) {
+ ASSERT_NOT_REACHED();
+ break;
+ }
+ if (first->style().display() == DisplayType::Ruby)
+ return downcast<RenderElement>(*first);
+ }
}
if (parent.style().display() != DisplayType::Ruby) {
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to a cross site scripting attack
Researchers: Johan Carlsson (joaxcar)
Key insight. An incomplete allow-list: the text/plain-preference safeguard forced XML and SVG back to inert text/plain but forgot application/xhtml+xml, so a text/plain response could still be reinterpreted as scriptable XHTML and run its embedded scripts – classic XSS via MIME confusion, fixed by completing the list.
Patch Details
diff --git a/LayoutTests/http/tests/mime/resources/.htaccess b/LayoutTests/http/tests/mime/resources/.htaccess
index 8c651044dc8c..3c1d95df81fa 100644
--- a/LayoutTests/http/tests/mime/resources/.htaccess
+++ b/LayoutTests/http/tests/mime/resources/.htaccess
@@ -1,6 +1,14 @@
<Files xml-with-html.xml>
ForceType "text/plain"
</Files>
+<Files xhtml-with-html.xhtml>
+ForceType "text/plain"
+</Files>
+<Files xhtml-with-html-with-content-disposition>
+ForceType "text/plain"
+Header always set Content-Disposition "inline; filename=\"xhtml-with-html-with-content-disposition.xhtml\""
+Header always set X-Content-Type-Options "nosniff"
+</Files>
<Files svg-with-html.svg>
ForceType "text/plain"
</Files>
diff --git a/LayoutTests/http/tests/mime/resources/xhtml-with-html-with-content-disposition b/LayoutTests/http/tests/mime/resources/xhtml-with-html-with-content-disposition
new file mode 100644
index 000000000000..91a7bdb2a4cc
--- /dev/null
+++ b/LayoutTests/http/tests/mime/resources/xhtml-with-html-with-content-disposition
@@ -0,0 +1,4 @@
+<?xml version=\'1.0\' encoding=\'utf-8\'?>
+<html xmlns:html="http://www.w3.org/1999/xhtml">
+<html:script>console.log("Fail");</html:script>
+</html>
diff --git a/LayoutTests/http/tests/mime/resources/xhtml-with-html.xhtml b/LayoutTests/http/tests/mime/resources/xhtml-with-html.xhtml
new file mode 100644
index 000000000000..91a7bdb2a4cc
--- /dev/null
+++ b/LayoutTests/http/tests/mime/resources/xhtml-with-html.xhtml
@@ -0,0 +1,4 @@
+<?xml version=\'1.0\' encoding=\'utf-8\'?>
+<html xmlns:html="http://www.w3.org/1999/xhtml">
+<html:script>console.log("Fail");</html:script>
+</html>
diff --git a/LayoutTests/http/tests/mime/xhtml-with-html-expected.txt b/LayoutTests/http/tests/mime/xhtml-with-html-expected.txt
new file mode 100644
index 000000000000..1771f28358a1
--- /dev/null
+++ b/LayoutTests/http/tests/mime/xhtml-with-html-expected.txt
@@ -0,0 +1,2 @@
+xhtml-with-html.xhtml has MIME type text/plain
+
diff --git a/LayoutTests/http/tests/mime/xhtml-with-html-with-content-disposition-expected.txt b/LayoutTests/http/tests/mime/xhtml-with-html-with-content-disposition-expected.txt
new file mode 100644
index 000000000000..caaaacfd863e
--- /dev/null
+++ b/LayoutTests/http/tests/mime/xhtml-with-html-with-content-disposition-expected.txt
@@ -0,0 +1,2 @@
+xhtml-with-html-with-content-disposition has MIME type text/plain
+
diff --git a/LayoutTests/http/tests/mime/xhtml-with-html-with-content-disposition.html b/LayoutTests/http/tests/mime/xhtml-with-html-with-content-disposition.html
new file mode 100644
index 000000000000..e29d3836b9ba
--- /dev/null
+++ b/LayoutTests/http/tests/mime/xhtml-with-html-with-content-disposition.html
@@ -0,0 +1,24 @@
+<html>
+<script>
+if (window.testRunner) {
+ testRunner.waitUntilDone();
+ testRunner.dumpResourceResponseMIMETypes();
+ testRunner.dumpAsText();
+}
+</script>
+<body>
+<script>
+function runTests() {
+ let iframe = document.createElement("iframe");
+ iframe.src = 'resources/xhtml-with-html-with-content-disposition';
+ if (window.testRunner) {
+ iframe.onerror = testRunner.notifyDone();
+ iframe.onload = testRunner.notifyDone();
+ }
+ document.body.appendChild(iframe);
+}
+runTests();
+</script>
+</body>
+</html>
+
diff --git a/LayoutTests/http/tests/mime/xhtml-with-html.html b/LayoutTests/http/tests/mime/xhtml-with-html.html
new file mode 100644
index 000000000000..41478413bcb6
--- /dev/null
+++ b/LayoutTests/http/tests/mime/xhtml-with-html.html
@@ -0,0 +1,24 @@
+<html>
+<script>
+if (window.testRunner) {
+ testRunner.waitUntilDone();
+ testRunner.dumpResourceResponseMIMETypes();
+ testRunner.dumpAsText();
+}
+</script>
+<body>
+<script>
+function runTests() {
+ let iframe = document.createElement("iframe");
+ iframe.src = 'resources/xhtml-with-html.xhtml';
+ if (window.testRunner) {
+ iframe.onerror = testRunner.notifyDone();
+ iframe.onload = testRunner.notifyDone();
+ }
+ document.body.appendChild(iframe);
+}
+runTests();
+</script>
+</body>
+</html>
+
diff --git a/Source/WebCore/platform/network/ios/WebCoreURLResponseIOS.mm b/Source/WebCore/platform/network/ios/WebCoreURLResponseIOS.mm
index 8822e84e8a7c..b07adf5849cc 100644
--- a/Source/WebCore/platform/network/ios/WebCoreURLResponseIOS.mm
+++ b/Source/WebCore/platform/network/ios/WebCoreURLResponseIOS.mm
@@ -40,7 +40,7 @@ namespace WebCore {
static inline bool shouldPreferTextPlainMIMEType(const String& mimeType, const String& proposedMIMEType)
{
- return ("text/plain"_s == mimeType) && ((proposedMIMEType == "text/xml"_s) || (proposedMIMEType == "application/xml"_s) || (proposedMIMEType == "image/svg+xml"_s));
+ return ("text/plain"_s == mimeType) && ((proposedMIMEType == "text/xml"_s) || (proposedMIMEType == "application/xhtml+xml"_s) || (proposedMIMEType == "application/xml"_s) || (proposedMIMEType == "image/svg+xml"_s));
}
void adjustMIMETypeIfNecessary(CFURLResponseRef response, bool isMainResourceLoad)
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: Seunghyun Lee (@0x10n) of KAIST Hacking Lab working with Trend Micro Zero Day Initiative
Background
Advisory: Apple Security Advisory
| Attribute | Value |
|---|---|
| CVE | CVE-2024-40789 |
| Bugzilla | None |
| Component | WebKit |
| Bug Class | OOB |
| Severity | medium |
| Advisory | Apple Advisory |
Root Cause Analysis
An out-of-bounds access issue was addressed with improved bounds checking.
Audit Directions
When auditing for this vulnerability, focus on: (1) similar patterns in the patched functions and nearby code, (2) missing or insufficient bounds checks on user-controlled input, (3) unvalidated type conversions and pointer arithmetic, (4) edge cases not exercised by tests, and (5) copy-paste variants of the vulnerable logic elsewhere.
Overview
Impact:
Description: Private Browsing tabs may be accessed without authentication
Researchers: Matthew Butler
Key insight. The root cause is a missing session-type policy check on a data path that crosses from a private (ephemeral) browsing context into persistent, system-visible UI; the fix is to gate Now Playing eligibility and metadata population on usesEphemeralSession() and mediaPlaybackIsSuspended() so private-session content never populates shared surfaces.
Patch Details
diff --git a/Source/WebCore/Modules/webaudio/AudioContext.cpp b/Source/WebCore/Modules/webaudio/AudioContext.cpp
index 4b55ff6f6e19..0f9a1164b0dc 100644
--- a/Source/WebCore/Modules/webaudio/AudioContext.cpp
+++ b/Source/WebCore/Modules/webaudio/AudioContext.cpp
@@ -524,6 +524,13 @@ bool AudioContext::isNowPlayingEligible() const
return false;
RefPtr document = this->document();
+ if (!document)
+ return false;
+
+ RefPtr page = document->page();
+ if (page && page->mediaPlaybackIsSuspended())
+ return false;
+
return hasPlayBackAudioSession(document.get());
}
@@ -553,6 +560,9 @@ std::optional<NowPlayingInfo> AudioContext::nowPlayingInfo() const
{ }
};
+ if (page->usesEphemeralSession())
+ return nowPlayingInfo;
+
#if ENABLE(MEDIA_SESSION)
if (RefPtr mediaSession = NavigatorMediaSession::mediaSessionIfExists(Ref { window->navigator() }))
mediaSession->updateNowPlayingInfo(nowPlayingInfo);
diff --git a/Source/WebCore/html/HTMLMediaElement.cpp b/Source/WebCore/html/HTMLMediaElement.cpp
index 12493772c729..75f0af913d5d 100644
--- a/Source/WebCore/html/HTMLMediaElement.cpp
+++ b/Source/WebCore/html/HTMLMediaElement.cpp
@@ -655,6 +655,15 @@ RefPtr<HTMLMediaElement> HTMLMediaElement::bestMediaElementForRemoteControls(Med
return selectedSession ? RefPtr { &downcast<MediaElementSession>(selectedSession.get())->element() } : nullptr;
}
+bool HTMLMediaElement::isNowPlayingEligible() const
+{
+ RefPtr page = document().page();
+ if (page && page->mediaPlaybackIsSuspended())
+ return false;
+
+ return m_mediaSession->hasNowPlayingInfo();
+}
+
std::optional<NowPlayingInfo> HTMLMediaElement::nowPlayingInfo() const
{
return m_mediaSession->computeNowPlayingInfo();
diff --git a/Source/WebCore/html/HTMLMediaElement.h b/Source/WebCore/html/HTMLMediaElement.h
index e4e90022ef85..2829f85687a2 100644
--- a/Source/WebCore/html/HTMLMediaElement.h
+++ b/Source/WebCore/html/HTMLMediaElement.h
@@ -960,7 +960,7 @@ private:
bool hasMediaStreamSource() const final;
void processIsSuspendedChanged() final;
bool shouldOverridePauseDuringRouteChange() const final;
- bool isNowPlayingEligible() const final { return m_mediaSession->hasNowPlayingInfo(); }
+ bool isNowPlayingEligible() const final;
std::optional<NowPlayingInfo> nowPlayingInfo() const final;
WeakPtr<PlatformMediaSession> selectBestMediaSession(const Vector<WeakPtr<PlatformMediaSession>>&, PlatformMediaSession::PlaybackControlsPurpose) final;
diff --git a/Source/WebCore/html/MediaElementSession.cpp b/Source/WebCore/html/MediaElementSession.cpp
index 1fe121b832f6..b6854f036586 100644
--- a/Source/WebCore/html/MediaElementSession.cpp
+++ b/Source/WebCore/html/MediaElementSession.cpp
@@ -1286,6 +1286,8 @@ std::optional<NowPlayingInfo> MediaElementSession::computeNowPlayingInfo() const
return { };
RefPtr page = m_element.document().page();
+ if (!page)
+ return { };
bool allowsNowPlayingControlsVisibility = page && !page->isVisibleAndActive();
bool isPlaying = state() == PlatformMediaSession::State::Playing;
@@ -1306,6 +1308,15 @@ std::optional<NowPlayingInfo> MediaElementSession::computeNowPlayingInfo() const
#endif
NowPlayingInfo info { m_element.mediaSessionTitle(), emptyString(), emptyString(), sourceApplicationIdentifier, duration, currentTime, rate, supportsSeeking, m_element.mediaUniqueIdentifier(), isPlaying, allowsNowPlayingControlsVisibility, { } };
+
+ if (page->usesEphemeralSession()) {
+ info.title = { };
+ info.artist = { };
+ info.album = { };
+ info.artwork = std::nullopt;
+ return info;
+ }
+
#if ENABLE(MEDIA_SESSION)
if (RefPtr session = mediaSession())
session->updateNowPlayingInfo(info);
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: Gary Kwong
Key insight. A missing isRefType operand check let ref.as_non_null accept non-reference values, creating an integer/reference type confusion in the Wasm engine; the fix is the classic add-the-validation restoration of the type invariant the compiler downstream assumed.
Patch Details
diff --git a/JSTests/wasm/stress/funcref-types.js b/JSTests/wasm/stress/funcref-types.js
new file mode 100644
index 000000000000..ce0e46a94e41
--- /dev/null
+++ b/JSTests/wasm/stress/funcref-types.js
@@ -0,0 +1,10 @@
+//@ requireOptions("--useWasmTypedFunctionReferences=1")
+var wasm_code = new Uint8Array([0x00,0x61,0x73,0x6D,0x01,0x00,0x00,0x00,0x01,0x9F,0x80,0x00,0x04,0x60,0x03,0x7F,0x6F,0x7F,0x01,0x7F,0x60,0x0C,0x7D,0x7F,0x7F,0x7F,0x7F,0x7E,0x7E,0x6F,0x7D,0x7F,0x7F,0x7D,0x00,0x60,0x02,0x7E,0x7D,0x00,0x60,0x00,0x00,0x03,0x82,0x80,0x00,0x01,0x03,0x04,0x85,0x80,0x00,0x01,0x70,0x01,0x01,0x01,0x0D,0x87,0x80,0x00,0x03,0x00,0x01,0x00,0x02,0x00,0x03,0x07,0x88,0x80,0x00,0x01,0x04,0x6D,0x61,0x69,0x6E,0x00,0x00,0x09,0x8B,0x80,0x00,0x01,0x06,0x00,0x41,0x00,0x0B,0x70,0x01,0xD2,0x00,0x0B,0x0A,0xF8,0x89,0x00,0x01,0xF5,0x09,0x05,0x25,0x7F,0x5A,0x7D,0x46,0x6F,0x34,0x7D,0x08,0x7F,0x43,0x36,0x37,0x0E,0x39,0x43,0x13,0x7D,0x14,0x7F,0x43,0x41,0x82,0x41,0xFF,0x43,0x01,0x7D,0x04,0x7F,0x43,0x30,0x38,0x2C,0x31,0x43,0x01,0x7C,0x04,0x7F,0x43,0x35,0x30,0x2C,0x8B,0x98,0x8F,0x8B,0x8F,0x8B,0x94,0x8B,0x8F,0x8B,0xD4,0x10,0x00,0x00,0x8F,0x8B,0x8F,0x8B,0x8F,0x8B,0x8B,0x91,0x90,0x8B,0x90,0x8F,0x8F,0x92,0x97,0x8B,0x8F,0x8B,0x8F,0x8B,0x97,0x91,0x43,0x36,0x37,0x1D,0x00,0x43,0x2C,0x00,0x33,0x00,0x94,0x91,0x8B,0x43,0x2C,0x30,0x2C,0x30,0x8F,0x8B,0x8F,0x8B,0x8F,0x92,0x44,0x5E,0x8B,0x90,0x8B,0x90,0x97,0x90,0x91,0x06,0x7D,0x43,0x2C,0x99,0xA1,0x7C,0x10,0x00,0x10,0x00,0x00,0x1A,0x1A,0x1A,0x1A,0x1A,0x1A,0x1A,0x1A,0x2D,0x19,0x43,0x6E,0x61,0x6D,0x65,0x0B,0x8B,0x91,0x90,0x06,0x7D,0x43,0x2C,0x30,0x00,0x00,0x20,0x41,0x08,0x02,0x40,0x00,0x1A,0x1A,0x1A,0x1A,0x1A,0x1A,0x1A,0x01,0x8C,0x00,0x00,0x01,0x19,0x43,0xEC,0x00,0x00,0x00,0x0B,0x8B,0x91,0x90,0x06,0x7D,0x43,0x2C,0x30,0x00,0x00,0x41,0x00,0x08,0x02,0x00,0x00,0x1A,0x1A,0x1A,0x1A,0x1A,0x1A,0x1A,0x01,0x19,0x43,0x00,0x00,0x00,0x00,0x0B,0x8F,0x8B,0x8B,0x8B,0x8F,0x8B,0x8F,0x8B,0x8F,0x8B,0x8F,0x8B,0x8F,0x8B,0x8F,0x8B,0x90,0x8B,0x43,0x08,0x00,0x01,0x00,0x01,0x8B,0x96,0x8B,0x8F,0x8F,0x91,0x20,0x00,0x25,0x00,0x00,0x00,0x1A,0x00,0x01,0x00,0x92,0x91,0x00,0x90,0x75,0x6F,0x6F,0x76,0x8F,0x85,0x8F,0x76,0x4D,0x4F,0x47,0x4E,0x41,0x7F,0xBF,0xFD,0x7E,0x73,0x41,0x00,0x0C,0x00,0x0B,0x78,0x4F,0x1F,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x8F,0x8B,0xBA,0x8A,0x9E,0x6F,0x9E,0x0E,0xC4,0x80,0x8F,0x8B,0x8F,0x8B,0x8F,0x8B,0x8B,0x8B,0x8F,0x72,0x8F,0x7F,0x8F,0x8B,0x95,0x8B,0x70,0x96,0xD0,0x94,0x6C,0x53,0x7F,0x7D,0xE7,0x63,0x91,0x7E,0x7D,0x00,0x60,0x02,0x00,0x03,0x01,0x80,0xC2,0x80,0xC2,0x80,0x0C,0x00,0x0B,0x78,0x4F,0x1F,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x8F,0x8B,0xBA,0x8A,0x9E,0x6F,0x9E,0x0E,0xC4,0x80,0x8F,0x8F,0x8B,0x8B,0x8F,0x8B,0x8B,0x8B,0x8F,0x72,0xDE,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x2F,0x63,0x6C,0x53,0x7F,0x7D,0xE7,0x63,0x91,0x7E,0x7D,0x00,0x60,0x02,0x97,0x01,0x00,0x00,0xC2,0x80,0xC2,0x80,0xC2,0x80,0x01,0xDE,0x81,0x00,0x00,0x00,0x17,0x00,0x01,0xB6,0x4E,0x2F,0x6C,0x69,0x67,0x6E,0x02,0x5F,0x43,0x62,0x17,0x72,0x74,0x2E,0xC2,0x80,0x00,0x80,0x8F,0x97,0x90,0x91,0x06,0x7D,0x43,0x2C,0x99,0x5A,0x83,0xEF,0xFF,0x10,0x00,0x00,0x1A,0x1A,0x1A,0x1A,0x1A,0x1A,0x1A,0x1A,0x01,0x19,0x43,0x6E,0x61,0x6D,0x65,0x0B,0x8B,0x91,0x90,0x06,0x7D,0x43,0x2C,0x30,0x00,0x00,0x20,0x41,0x08,0x02,0x40,0x00,0x1A,0x1A,0x1A,0x1A,0x1A,0x1A,0x1A,0x01,0x8C,0x00,0x00,0x01,0x19,0x43,0xEC,0x00,0x00,0x00,0x91,0x7D,0x06,0x90,0x8B,0x0B,0x43,0x2C,0x30,0x00,0x00,0x41,0x00,0x08,0x02,0x00,0x00,0x1A,0x1A,0x1A,0x1A,0x1A,0x1A,0x1A,0x01,0x19,0x43,0x00,0x00,0x00,0x00,0x0B,0x8F,0x8B,0x8B,0x8B,0x8F,0x8B,0x01,0x8F,0x19,0x43,0x00,0x00,0x00,0x00,0x0B,0x8F,0x8B,0x8B,0x8B,0x8F,0x8B,0x8F,0x8B,0x8F,0x8B,0x8F,0x8B,0x8F,0x8B,0x8F,0x8B,0x90,0x8B,0x43,0x08,0x00,0x01,0x00,0x01,0x8B,0x96,0x8B,0x8F,0x8F,0x91,0x20,0x00,0x25,0x00,0x00,0x00,0x1A,0x00,0x01,0x00,0x92,0x91,0x00,0x90,0x75,0x6F,0x6F,0x77,0x8F,0x85,0x8F,0x76,0x4D,0x4F,0x47,0x4E,0x41,0x7F,0xBF,0xFD,0x7E,0x73,0x41,0x00,0x0C,0x00,0x0B,0x78,0x4F,0x1F,0x00,0xFF,0xFF,0xFF,0xFF,0x00,0x00,0x8F,0x8B,0xBA,0x8A,0x9E,0x6F,0x9E,0x0E,0xC4,0x80,0x8F,0x8B,0x8F,0x8B,0x8F,0x8B,0x8B,0x8B,0x8F,0x72,0x8F,0x7F,0x8F,0x8B,0x95,0x8B,0x90,0x2E,0x2F,0x63,0x6C,0x53,0x7F,0x7D,0xE7,0x63,0x91,0x7E,0x7D,0x00,0x60,0x02,0x00,0x03,0x01,0x80,0xC2,0x80,0xC2,0x80,0x0C,0x00,0x0B,0x78,0x4F,0x1F,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x8F,0x8B,0xBA,0x8A,0x9E,0x6F,0x9E,0x0E,0xC4,0x80,0x8F,0x8F,0x8B,0x8B,0x8F,0x2C,0x31,0x00,0x00,0x39,0x2C,0x31,0x00,0x00,0xB0,0x01,0x00,0x00,0x00,0x95,0x43,0x2F,0x6C,0x69,0x62,0x63,0x6C,0x61,0x6E,0x67,0x5F,0x72,0x74,0x00,0xFD,0x3C,0x30,0x00,0x2A,0x5D,0x00,0x00,0x00,0x96,0x00,0xE0,0x04,0x32,0x9F,0x20,0x00,0x96,0x91,0x43,0x22,0x23,0x7F,0x41,0x92,0x95,0x91,0xB0,0x82,0x69,0x91,0x4B,0x8B,0x43,0x11,0x00,0x00,0x00,0x00,0xF6,0x01,0xEC,0xEE,0x92,0x95,0x91,0x90,0x7D,0xFF,0xFF,0xFF,0x9E,0x43,0x19,0x36,0x00,0x00,0x43,0x32,0x27,0x00,0xF4,0x43,0x31,0x43,0x30,0x61,0x23,0x40,0x5F,0x72,0x74,0x00,0xFD,0x3C,0x3F,0x09,0x00,0x92,0x00,0x00,0x00,0x96,0x92,0x90,0x43,0x00,0x00,0x09,0x22,0x43,0x00,0x00,0x00,0x00,0x3B,0x32,0x9F,0x00,0x00,0x96,0x91,0x43,0x2C,0x31,0x32,0x18,0x92,0x95,0x6F,0x62,0x6A,0x65,0x63,0x02,0x7E,0x7D,0x00,0x60,0x00,0x00,0x03,0x82,0x80,0x00,0x09,0x00,0x04,0x85,0x80,0x00,0x01,0x70,0x01,0x01,0x01,0x05,0x84,0x80,0x0A,0x3F,0xFC,0x43,0x32,0x80,0x10,0x0A,0x1A,0x00,0x43,0x2C,0x31,0x32,0x10,0xCF,0x8B,0x8F,0x0A,0x06,0x8B,0x8F,0x8B,0xCF,0x8B,0x8F,0x8B,0x95,0x8B,0xA2,0x2F,0x6C,0x69,0x62,0xFD,0x6C,0x22,0xF9,0x00,0x00,0x00,0x00,0x1C,0x62,0x1C,0x00,0x00,0x95,0x2C,0x31,0x32,0x00,0x96,0x94,0x91,0x90,0x7E,0x96,0x91,0x30,0x40,0x06,0x00,0x00,0x42,0x2C,0x65,0x91,0x92,0x90,0x00,0x00,0x43,0x00,0x00,0x00,0x00,0x43,0x9F,0x00,0x29,0x00,0x00,0x30,0xFB,0x00,0x00,0x81,0xB5,0x00,0x00,0x00,0x00,0x00,0x00,0x95,0x91,0x90,0x0B,0x6D,0x79,0x10,0x37,0x06,0x80,0xB3,0x61,0x6D,0x73,0x00,0x01,0x00,0x00,0x00,0x08,0x40,0x3F,0x5F,0x43,0x46,0x10,0x90,0x00,0x00,0x43,0x2C,0x31,0x42,0x23,0x0E,0x1A,0x95,0x43,0x2C,0x30,0x00,0x00,0x43,0x2C,0x70,0x00,0x00,0x43,0x32,0x30,0x00,0x00,0x95,0x43,0x2F,0x6C,0x69,0x62,0x63,0x01,0x00,0x00,0x00,0x72,0x00,0x96,0x91,0x95,0x43,0x2C,0x2F,0x6B,0x69,0x62,0x10,0x07,0x3F,0x80,0x67,0x5F,0x72,0x7E,0x31,0x31,0x00,0x00,0x77,0x91,0x53,0x74,0x96,0x00,0x32,0x31,0x94,0x91,0x90,0x86,0x81,0x07,0x43,0x00,0x00,0x00,0x40,0x00,0xC0,0xA1,0xFA,0x5B,0x3E,0x00,0x00,0x43,0x00,0xD9,0x00,0x00,0x00,0x00,0x29,0x76,0x00,0x00,0x00,0x00,0xFD,0x00,0xFF,0xFF,0x00,0x00,0x00,0x8E,0x6F,0x6F,0xF4,0x92,0x86,0x68,0xC9,0xFF,0x80,0xB3,0x00,0x61,0x73,0x7D,0x01,0x00,0x00,0x00,0x08,0x40,0x5F,0x5F,0x43,0x46,0x10,0x90,0x00,0x00,0x43,0x2C,0x31,0x38,0x03,0x00,0x00,0x95,0x43,0xB6,0x07,0x24,0x4F,0x00,0x00,0x01,0xEC,0x00,0x00,0x00,0x18,0x02,0x8F,0x8B,0x8F,0x8B,0x8F,0x8B,0x8F,0x8E,0x8B,0x97,0x91,0x43,0x31,0x38,0x1D,0x00,0x43,0x28,0x00,0x01,0x00,0x94,0x91,0x8B,0x43,0x30,0xBC,0x2C,0x2C,0x00,0x43,0xE7,0xFF,0xFF,0x91,0x91,0xCB,0x43,0x2C,0x30,0x2C,0x30,0x8F,0x8B,0x96,0x8B,0x8F,0x8F,0x91,0x3B,0x00,0x25,0x00,0x00,0x00,0x8B,0x8F,0x8B,0x8F,0x8B,0x8F,0x8B,0x8F,0x8B,0x8F,0x8B,0x8F,0x8B,0x8F,0x8B,0xE7,0x8B,0x0F,0x8B,0x8F,0x8B,0x92,0x90,0x8B,0x8D,0x93,0x8B,0x43,0x30,0x2C,0x2A,0x14,0xC1,0x8F,0x8B,0x8F,0x05,0x46,0x9C,0xAF,0x40,0x90,0x6F,0x74,0x96,0x91,0xD2,0xEF,0x6F,0x72,0x74,0xFF,0xB6,0x06,0xFD,0x3F,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xF5,0x00,0x00,0x00,0x08,0x00,0x00,]);
+try {
+var wasm_module = new WebAssembly.Module(wasm_code);
+var wasm_instance = new WebAssembly.Instance(wasm_module);
+var f = wasm_instance.exports.main;
+f();
+}
+catch (e) {
+}
diff --git a/Source/JavaScriptCore/wasm/WasmFunctionParser.h b/Source/JavaScriptCore/wasm/WasmFunctionParser.h
index 69039e35970b..e22389e044ec 100644
--- a/Source/JavaScriptCore/wasm/WasmFunctionParser.h
+++ b/Source/JavaScriptCore/wasm/WasmFunctionParser.h
@@ -2753,6 +2753,7 @@ FOR_EACH_WASM_MEMORY_STORE_OP(CREATE_CASE)
WASM_PARSER_FAIL_IF(!Options::useWasmTypedFunctionReferences(), "function references are not enabled"_s);
TypedExpression ref;
WASM_TRY_POP_EXPRESSION_STACK_INTO(ref, "ref.as_non_null"_s);
+ WASM_VALIDATOR_FAIL_IF(!isRefType(ref.type()), "ref.as_non_null ref to type ", ref.type(), " expected a reference type");
ExpressionType result;
WASM_TRY_ADD_TO_CONTEXT(addRefAsNonNull(ref, result));
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: A user may be able to bypass some web content restrictions
Researchers: Andreas Jaegersberger and Ro Achterberg
Background
Advisory: Apple Security Advisory
| Attribute | Value |
|---|---|
| CVE | CVE-2024-44206 |
| Bugzilla | 280765 |
| Component | WebKit |
| Bug Class | LogicError |
| Severity | high |
| Advisory | Apple Advisory |
Fix not public. The WebKit fix for this bug is embargoed or not yet disclosed. The bug ID is confirmed from the Apple advisory, but no public commit references it.
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected process crash
Researchers: An anonymous researcher
Key insight. The compressed-texture upload path forgot the pixel-unpack state synchronization that every uncompressed path performs, so the driver read the source data with mismatched geometry; the one-line fix simply restores parity by calling setPixelUnpackState before the native compressed upload.
Patch Details
diff --git a/Source/ThirdParty/ANGLE/src/libANGLE/renderer/gl/TextureGL.cpp b/Source/ThirdParty/ANGLE/src/libANGLE/renderer/gl/TextureGL.cpp
index c659aacb9e48..f96eefe53f11 100644
--- a/Source/ThirdParty/ANGLE/src/libANGLE/renderer/gl/TextureGL.cpp
+++ b/Source/ThirdParty/ANGLE/src/libANGLE/renderer/gl/TextureGL.cpp
@@ -664,6 +664,7 @@ angle::Result TextureGL::setCompressedSubImage(const gl::Context *context,
nativegl::GetCompressedSubTexImageFormat(functions, features, format);
stateManager->bindTexture(getType(), mTextureID);
+ ANGLE_TRY(stateManager->setPixelUnpackState(context, unpack));
if (nativegl::UseTexImage2D(getType()))
{
ASSERT(area.z == 0 && area.depth == 1);
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing web content may lead to a denial-of-service
Researchers: ajajfxhj
Key insight. Deferred main-thread tasks captured a raw ’this’ with no tie to the manager’s lifetime, so destroying the manager before the task ran caused a use-after-free; the fix routes every such task through a TaskCancellationGroup cancelled in the destructor, restoring the object-alive-when-task-runs invariant.
Patch Details
diff --git a/Source/WebCore/platform/audio/PlatformMediaSessionManager.cpp b/Source/WebCore/platform/audio/PlatformMediaSessionManager.cpp
index 81dc8e026141..999b08a1e845 100644
--- a/Source/WebCore/platform/audio/PlatformMediaSessionManager.cpp
+++ b/Source/WebCore/platform/audio/PlatformMediaSessionManager.cpp
@@ -117,6 +117,11 @@ PlatformMediaSessionManager::PlatformMediaSessionManager()
{
}
+PlatformMediaSessionManager::~PlatformMediaSessionManager()
+{
+ m_taskGroup.cancel();
+}
+
static inline unsigned indexFromMediaType(PlatformMediaSession::MediaType type)
{
return static_cast<unsigned>(type);
@@ -492,7 +497,7 @@ void PlatformMediaSessionManager::sessionCanProduceAudioChanged()
return;
m_alreadyScheduledSessionStatedUpdate = true;
- callOnMainThread([this] {
+ enqueueTaskOnMainThread([this] {
m_alreadyScheduledSessionStatedUpdate = false;
maybeActivateAudioSession();
updateSessionState();
@@ -656,7 +661,7 @@ void PlatformMediaSessionManager::scheduleUpdateSessionState()
return;
m_hasScheduledSessionStateUpdate = true;
- callOnMainThread([this] {
+ enqueueTaskOnMainThread([this] {
updateSessionState();
m_hasScheduledSessionStateUpdate = false;
});
@@ -903,6 +908,13 @@ bool PlatformMediaSessionManager::hasActiveNowPlayingSessionInGroup(MediaSession
return hasActiveNowPlayingSession;
}
+void PlatformMediaSessionManager::enqueueTaskOnMainThread(Function<void()>&& task)
+{
+ callOnMainThread(CancellableTask(m_taskGroup, [task = WTFMove(task)] () mutable {
+ task();
+ }));
+}
+
#if !RELEASE_LOG_DISABLED
WTFLogChannel& PlatformMediaSessionManager::logChannel() const
{
diff --git a/Source/WebCore/platform/audio/PlatformMediaSessionManager.h b/Source/WebCore/platform/audio/PlatformMediaSessionManager.h
index 527d12fff117..e5dd5c0f9503 100644
--- a/Source/WebCore/platform/audio/PlatformMediaSessionManager.h
+++ b/Source/WebCore/platform/audio/PlatformMediaSessionManager.h
@@ -31,6 +31,7 @@
#include "RemoteCommandListener.h"
#include "Timer.h"
#include <wtf/AggregateLogger.h>
+#include <wtf/CancellableTask.h>
#include <wtf/Vector.h>
#include <wtf/WeakHashSet.h>
#include <wtf/WeakPtr.h>
@@ -83,7 +84,7 @@ class PlatformMediaSessionManager
WEBCORE_EXPORT static void setMediaCapabilityGrantsEnabled(bool);
#endif
- virtual ~PlatformMediaSessionManager() = default;
+ virtual ~PlatformMediaSessionManager();
virtual void scheduleSessionStatusUpdate() { }
@@ -227,6 +228,7 @@ class PlatformMediaSessionManager
std::optional<bool> supportsSpatialAudioPlayback() { return m_supportsSpatialAudioPlayback; }
void nowPlayingMetadataChanged(const NowPlayingMetadata&);
+ void enqueueTaskOnMainThread(Function<void()>&&);
private:
friend class Internals;
@@ -260,6 +262,7 @@ class PlatformMediaSessionManager
bool m_hasScheduledSessionStateUpdate { false };
WeakHashSet<NowPlayingMetadataObserver> m_nowPlayingMetadataObservers;
+ TaskCancellationGroup m_taskGroup;
#if ENABLE(WEBM_FORMAT_READER)
static bool m_webMFormatReaderEnabled;
diff --git a/Source/WebCore/platform/audio/cocoa/MediaSessionManagerCocoa.mm b/Source/WebCore/platform/audio/cocoa/MediaSessionManagerCocoa.mm
index 3c4000a571a1..d50c37334a7e 100644
--- a/Source/WebCore/platform/audio/cocoa/MediaSessionManagerCocoa.mm
+++ b/Source/WebCore/platform/audio/cocoa/MediaSessionManagerCocoa.mm
@@ -270,7 +270,7 @@
void MediaSessionManagerCocoa::scheduleSessionStatusUpdate()
{
- callOnMainThread([this] () mutable {
+ enqueueTaskOnMainThread([this] () mutable {
m_nowPlayingManager->setSupportsSeeking(computeSupportsSeeking());
updateNowPlayingInfo();
@@ -329,7 +329,7 @@
{
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing web content may lead to arbitrary code execution
Researchers: Lukas Bernhard of CISPA Helmholtz Center for Information Security
Key insight. AccessCase cached a raw CallLinkInfo* owned by the JITStubRoutine, and once CodeBlock destruction could be delayed that pointer could dangle and be dereferenced during GC weak-visiting; moving CallLinkInfo lifetime into MarkingGCAwareJITStubRoutine and removing it from AccessCase fixes the UAF.
Patch Details
diff --git a/JSTests/stress/decouple-calllinkinfo-from-access-case.js b/JSTests/stress/decouple-calllinkinfo-from-access-case.js
new file mode 100644
index 000000000000..a7fd1f75b1ab
--- /dev/null
+++ b/JSTests/stress/decouple-calllinkinfo-from-access-case.js
@@ -0,0 +1,96 @@
+// runDefault("--validateOptions=true", "--thresholdForJITSoon=10", "--thresholdForJITAfterWarmUp=10", "--thresholdForOptimizeAfterWarmUp=100", "--thresholdForOptimizeAfterLongWarmUp=100", "--thresholdForOptimizeSoon=100", "--thresholdForFTLOptimizeAfterWarmUp=1000", "--thresholdForFTLOptimizeSoon=1000", "--validateBCE=true")
+
+const ProxyConstructor = Proxy;
+const getPrototypeOf = Object.getPrototypeOf;
+const ReflectGet = Reflect.get;
+const ReflectSet = Reflect.set;
+const ReflectHas = Reflect.has;
+const setPrototypeOf = Object.setPrototypeOf;
+
+function probe(id, value) {
+ let originalPrototype, newPrototype;
+ let handler = {
+ get(target, key, receiver) {
+ if (key === '__proto__' && receiver === value) return originalPrototype;
+ if (receiver === newPrototype) return ReflectGet(target, key);
+ return ReflectGet(target, key, receiver);
+ },
+ set(target, key, value, receiver) {
+ if (receiver === newPrototype) return ReflectSet(target, key, value);
+ return ReflectSet(target, key, value, receiver);
+ },
+ has(target, key) {
+ return ReflectHas(target, key);
+ },
+ };
+
+ try {
+ originalPrototype = getPrototypeOf(value);
+ newPrototype = new ProxyConstructor(originalPrototype, handler);
+ setPrototypeOf(value, newPrototype);
+ } catch (e) {}
+}
+
+probe("v1", "2003629588");
+let v4 = 9150;
+v4--;
+probe("v6", 51828);
+function F7(a9, a10, a11) {
+ if (!new.target) { throw 'must be called with new'; }
+ const v12 = this?.constructor;
+ try { new v12(this, "object", 447824390); } catch (e) {}
+ a11 % a11;
+ this.b = a9;
+ this.g = a10;
+}
+const v15 = new F7("2003629588", "object", 447824390);
+const v16 = new F7(v15, v4, v4);
+const v17 = new F7("2003629588", 51828, 51828);
+probe("v17", v17);
+const v18 = v17?.constructor;
+probe("v18", v18);
+let v19;
+try { v19 = new v18("r", v17, "r"); } catch (e) {}
+probe("v19", v19);
+const v20 = [v17,v17];
+probe("v20", v20);
+const v21 = [F7,v15,v20,v15,v4];
+const v22 = [v4,"object",51828];
+probe("v22", v22);
+let v23;
+try { v23 = v22.reduce(v15); } catch (e) {}
+const v24 = [2,-354747782,-16,10251,-1485280459,5,6,536870888,-47153,-193790246];
+probe("v24", v24);
+function f25(a26, a27) {
+ const o28 = {
+ [a27]: a26,
+ "d": v21,
+ };
+ return o28;
+}
+f25(v16, v22);
+f25(v23, v16);
+f25(v15, v22);
+v24[4];
+function f33(a34, a35, a36, a37) {
+ probe("v36", a36);
+ ~a35;
+ v22.length = 1;
+ a36?.[v21];
+}
+v24.flatMap(f33);
+gc();
+class C20 {
+ valueOf(a22, a23) {
+ return ("n")[1204] - this;
+ }
+}
+const v26 = new C20();
+function f27(a28, a29) {
+ new BigInt64Array(3603);
+ return v26 * v26;
+}
+try {
+v26[Symbol.toPrimitive] = f27;
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing web content may lead to arbitrary code execution
Researchers: Jeff Johnson of underpassapp.com
Key insight. A HashMap lookup was only ASSERTed non-end and then dereferenced, so in release builds a missing async-call entry dereferenced the end() iterator; a runtime it != end() guard fixes it.
Patch Details
diff --git a/Source/JavaScriptCore/inspector/agents/InspectorDebuggerAgent.cpp b/Source/JavaScriptCore/inspector/agents/InspectorDebuggerAgent.cpp
index 24352a97dc76..e46f788274a8 100644
--- a/Source/JavaScriptCore/inspector/agents/InspectorDebuggerAgent.cpp
+++ b/Source/JavaScriptCore/inspector/agents/InspectorDebuggerAgent.cpp
@@ -444,7 +444,8 @@ void InspectorDebuggerAgent::didScheduleAsyncCall(JSC::JSGlobalObject* globalObj
if (!m_currentAsyncCallIdentifierStack.isEmpty()) {
auto it = m_pendingAsyncCalls.find(m_currentAsyncCallIdentifierStack.last());
ASSERT(it != m_pendingAsyncCalls.end());
- parentStackTrace = it->value;
+ if (LIKELY(it != m_pendingAsyncCalls.end()))
+ parentStackTrace = it->value;
}
auto identifier = asyncCallIdentifier(asyncCallType, callbackId);
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: A maliciously crafted webpage may be able to fingerprint the user
Researchers: Joe Rutkowski (@Joe12387) of Crawless and @abrahamjuliot
Key insight. The canvas noise-injection salt and privacy flags were plumbed to dedicated workers but not to shared/service workers, whose remote Page had neither, so OffscreenCanvas readback there was un-noised; extending the plumbing restores noise in all worker types.
Patch Details
diff --git a/Source/WTF/wtf/PlatformEnable.h b/Source/WTF/wtf/PlatformEnable.h
index 38d367cb578c..eef9de469a96 100644
--- a/Source/WTF/wtf/PlatformEnable.h
+++ b/Source/WTF/wtf/PlatformEnable.h
@@ -421,10 +421,6 @@
#define ENABLE_MOUSE_FORCE_EVENTS 1
#endif
-#if !defined(ENABLE_NETSCAPE_PLUGIN_METADATA_CACHE)
-#define ENABLE_NETSCAPE_PLUGIN_METADATA_CACHE 0
-#endif
-
#if !defined(ENABLE_NOTIFICATION_EVENT)
#define ENABLE_NOTIFICATION_EVENT 0
#endif
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to arbitrary code execution
Researchers: Manfred Paul (@_manfp) working with Trend Micro Zero Day Initiative
Key insight. Bitfield-lowering range checks (lsb + width > datasize, and datasize - amount1) used unchecked 64-bit arithmetic, so overflow/underflow let an out-of-range field slip through; safeAdd plus an amount1 < datasize guard restores the bounds.
Patch Details
diff --git a/JSTests/stress/sbfx-offset-overflow.js b/JSTests/stress/sbfx-offset-overflow.js
new file mode 100644
index 000000000000..9d7f7cd7790a
--- /dev/null
+++ b/JSTests/stress/sbfx-offset-overflow.js
@@ -0,0 +1,16 @@
+function foo(a,b,c) { let x = a | 0; let y = b | 0; let z = c &15;
+z = (x<<y)^(x<<(y&0x10ff)); let r = z^0xf01;
+let s = z^0xf1f;
+return (((a>>>r)<<s)>>s);
+}
+let LEN = 100000000-1;
+let res = 0;
+res = foo((LEN&127),456,789);
+
+if (res != -1)
+ throw "Wrong result: " + res
+
+for (let i = 0; i <= LEN; i++) res = foo((i&127),456,789);
+
+if (res != -1)
+ throw "Wrong result: " + res
\ No newline at end of file
diff --git a/Source/JavaScriptCore/b3/B3LowerToAir.cpp b/Source/JavaScriptCore/b3/B3LowerToAir.cpp
index 33adcbd1c25a..e8ff89dc84c7 100644
--- a/Source/JavaScriptCore/b3/B3LowerToAir.cpp
+++ b/Source/JavaScriptCore/b3/B3LowerToAir.cpp
@@ -3301,7 +3301,8 @@ class LowerToAir {
return false;
uint64_t width = WTF::bitCount(mask);
uint64_t datasize = opcode == ExtractUnsignedBitfield32 ? 32 : 64;
- if (lsb + width > datasize)
+ uint64_t resultDataSize = 0;
+ if (!WTF::safeAdd(lsb, width, resultDataSize) || resultDataSize > datasize)
return false;
append(opcode, tmp(srcValue), imm(lsbValue), imm(width), tmp(m_value));
@@ -3390,9 +3391,8 @@ class LowerToAir {
uint64_t highWidth = highWidthValue->asInt();
uint64_t lowWidth = lowWidthValue->asInt();
uint64_t datasize = opcode == ExtractRegister32 ? 32 : 64;
- // Note that when `lowWidth == datasize` we cannot turn it to `MOV Rd Rn` since
- // `m >>> lowWidth` means `m >>> (lowWidth % datasize)` in JavaScript.
- if (lowWidth + highWidth != datasize || maskBitCount != lowWidth || lowWidth == datasize)
+ uint64_t resultWidth = 0;
+ if (!WTF::safeAdd(lowWidth, highWidth, resultWidth) || resultWidth != datasize || maskBitCount != lowWidth || lowWidth == datasize)
return false;
ASSERT(lowWidth < datasize);
@@ -3429,7 +3429,8 @@ class LowerToAir {
return false;
uint64_t datasize = opcode == InsertBitField32 ? 32 : 64;
uint64_t width = WTF::bitCount(mask1);
- if (lsb + width > datasize)
+ uint64_t resultDataSize = 0;
+ if (!WTF::safeAdd(lsb, width, resultDataSize) || resultDataSize > datasize)
return false;
uint64_t mask2 = maskValue2->asInt();
@@ -3479,7 +3480,8 @@ class LowerToAir {
return false;
uint64_t width = WTF::bitCount(mask1);
uint64_t datasize = opcode == ExtractInsertBitfieldAtLowEnd32 ? 32 : 64;
- if (lsb + width > datasize)
+ uint64_t resultDataSize = 0;
+ if (!WTF::safeAdd(lsb, width, resultDataSize) || resultDataSize > datasize)
return false;
uint64_t mask2 = maskValue2->asInt();
@@ -3653,7 +3655,8 @@ class LowerToAir {
uint64_t width = WTF::bitCount(mask);
uint64_t datasize = opcode == InsertUnsignedBitfieldInZero32 ? 32 : 64;
- if (lsb + width > datasize)
+ uint64_t resultDataSize = 0;
+ if (!WTF::safeAdd(lsb, width, resultDataSize) || resultDataSize > datasize)
return false;
append(opcode, tmp(nValue), imm(right), imm(width), tmp(m_value));
@@ -3715,8 +3718,13 @@ class LowerToAir {
uint64_t amount2 = amount2Value->asInt();
uint64_t lsb = lsbValue->asInt();
uint64_t datasize = opcode == InsertSignedBitfieldInZero32 ? 32 : 64;
+
+ if (amount1 >= datasize)
+ return false;
+
uint64_t width = datasize - amount1;
- if (amount1 != amount2 || !width || lsb + width > datasize)
+ uint64_t resultDataSize = 0;
+ if (!WTF::safeAdd(lsb, width, resultDataSize) || amount1 != amount2 || !width || resultDataSize > datasize)
return false;
append(opcode, tmp(srcValue), imm(lsbValue), imm(width), tmp(m_value));
@@ -3763,8 +3771,13 @@ class LowerToAir {
uint64_t amount2 = amount2Value->asInt();
uint64_t lsb = lsbValue->asInt();
uint64_t datasize = opcode == ExtractSignedBitfield32 ? 32 : 64;
+
+ if (amount1 >= datasize)
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: An attacker with arbitrary read and write capability may be able to bypass Pointer Authentication
Researchers: Manfred Paul (@_manfp) working with Trend Micro’s Zero Day Initiative
Key insight. The JIT re-signed return PCs using an attacker-controllable modifier register (a random tag, or GPR temporaries), making the signing sequence a PAC-forging gadget; signing with a zero modifier (pacizb) or the stack pointer removes the controllable input.
Patch Details
diff --git a/Source/JavaScriptCore/assembler/MacroAssemblerARM64E.h b/Source/JavaScriptCore/assembler/MacroAssemblerARM64E.h
index 6925f04bc3d6..3f9233b2de58 100644
--- a/Source/JavaScriptCore/assembler/MacroAssemblerARM64E.h
+++ b/Source/JavaScriptCore/assembler/MacroAssemblerARM64E.h
@@ -65,6 +65,12 @@ class MacroAssemblerARM64E : public MacroAssemblerARM64 {
ALWAYS_INLINE void tagPtr(PtrTag tag, RegisterID target)
{
+ if (!tag) {
+ m_assembler.pacizb(target);
+ return;
+ }
+
+ RELEASE_ASSERT(Options::allowNonSPTagging());
auto tagGPR = getCachedDataTempRegisterIDAndInvalidate();
move(TrustedImm64(tag), tagGPR);
m_assembler.pacib(target, tagGPR);
@@ -76,11 +82,17 @@ class MacroAssemblerARM64E : public MacroAssemblerARM64 {
m_assembler.pacibsp();
return;
}
+ RELEASE_ASSERT(Options::allowNonSPTagging());
m_assembler.pacib(target, tag);
}
ALWAYS_INLINE void untagPtr(PtrTag tag, RegisterID target)
{
+ if (!tag) {
+ m_assembler.autizb(target);
+ return;
+ }
+
auto tagGPR = getCachedDataTempRegisterIDAndInvalidate();
move(TrustedImm64(tag), tagGPR);
m_assembler.autib(target, tagGPR);
diff --git a/Source/JavaScriptCore/dfg/DFGOSRExitCompilerCommon.cpp b/Source/JavaScriptCore/dfg/DFGOSRExitCompilerCommon.cpp
index 4e0d005cc08a..a597735bfaf7 100644
--- a/Source/JavaScriptCore/dfg/DFGOSRExitCompilerCommon.cpp
+++ b/Source/JavaScriptCore/dfg/DFGOSRExitCompilerCommon.cpp
@@ -254,6 +254,16 @@ void reifyInlinedCallFrames(CCallHelpers& jit, const OSRExitBase& exit)
ASSERT(JITCode::isBaselineCode(jit.baselineCodeBlock()->jitType()));
jit.storePtr(AssemblyHelpers::TrustedImmPtr(jit.baselineCodeBlock()), AssemblyHelpers::addressFor(CallFrameSlot::codeBlock));
+ GPRReg returnPCReg = GPRInfo::regT5;
+#if CPU(ARM64E)
+ GPRReg signingTagReg = GPRInfo::regT2;
+ if (!Options::allowNonSPTagging()) {
+ returnPCReg = ARM64Registers::lr;
+ signingTagReg = MacroAssembler::stackPointerRegister;
+ // We could save/restore lr here but we don't need to because the LLInt/Baseline will load it from the stack before returning anyway.
+ }
+#endif
+
const CodeOrigin* codeOrigin;
for (codeOrigin = &exit.m_codeOrigin; codeOrigin && codeOrigin->inlineCallFrame(); codeOrigin = codeOrigin->inlineCallFrame()->getCallerSkippingTailCalls()) {
InlineCallFrame* inlineCallFrame = codeOrigin->inlineCallFrame();
@@ -266,15 +276,25 @@ void reifyInlinedCallFrames(CCallHelpers& jit, const OSRExitBase& exit)
if (!trueCaller) {
ASSERT(inlineCallFrame->isTail());
- jit.loadPtr(AssemblyHelpers::Address(GPRInfo::callFrameRegister, CallFrame::returnPCOffset()), GPRInfo::regT3);
+ jit.loadPtr(AssemblyHelpers::Address(GPRInfo::callFrameRegister, CallFrame::returnPCOffset()), returnPCReg);
#if CPU(ARM64E)
+ if (!Options::allowNonSPTagging()) {
+ JIT_COMMENT(jit, "lldb dynamic execution / posix signals could trash your stack"); // We don't have to worry about signals because they shouldn't fire in WebContent process in this window.
+ jit.move(MacroAssembler::stackPointerRegister, GPRInfo::regT4);
+ }
+
jit.addPtr(AssemblyHelpers::TrustedImm32(sizeof(CallerFrameAndPC)), GPRInfo::callFrameRegister, GPRInfo::regT2);
- jit.untagPtr(GPRInfo::regT2, GPRInfo::regT3);
- jit.addPtr(AssemblyHelpers::TrustedImm32(inlineCallFrame->returnPCOffset() + sizeof(void*)), GPRInfo::callFrameRegister, GPRInfo::regT2);
- jit.validateUntaggedPtr(GPRInfo::regT3, GPRInfo::regT4);
- jit.tagPtr(GPRInfo::regT2, GPRInfo::regT3);
+ jit.untagPtr(GPRInfo::regT2, returnPCReg);
+ jit.validateUntaggedPtr(returnPCReg, GPRInfo::regT2);
+ jit.addPtr(AssemblyHelpers::TrustedImm32(inlineCallFrame->returnPCOffset() + sizeof(CPURegister)), GPRInfo::callFrameRegister, signingTagReg);
+ jit.tagPtr(signingTagReg, returnPCReg);
+
+ if (!Options::allowNonSPTagging()) {
+ JIT_COMMENT(jit, "lldb dynamic execution / posix signals are ok again");
+ jit.move(GPRInfo::regT4, MacroAssembler::stackPointerRegister);
+ }
#endif
- jit.storePtr(GPRInfo::regT3, AssemblyHelpers::addressForByteOffset(inlineCallFrame->returnPCOffset()));
+ jit.storePtr(returnPCReg, AssemblyHelpers::addressForByteOffset(inlineCallFrame->returnPCOffset()));
jit.loadPtr(AssemblyHelpers::Address(GPRInfo::callFrameRegister, CallFrame::callerFrameOffset()), GPRInfo::regT3);
callerFrameGPR = GPRInfo::regT3;
} else {
@@ -291,10 +311,20 @@ void reifyInlinedCallFrames(CCallHelpers& jit, const OSRExitBase& exit)
}
#if CPU(ARM64E)
- jit.addPtr(AssemblyHelpers::TrustedImm32(inlineCallFrame->returnPCOffset() + sizeof(void*)), GPRInfo::callFrameRegister, GPRInfo::regT2);
- jit.move(AssemblyHelpers::TrustedImmPtr(jumpTarget.untaggedPtr()), GPRInfo::regT4);
- jit.tagPtr(GPRInfo::regT2, GPRInfo::regT4);
- jit.storePtr(GPRInfo::regT4, AssemblyHelpers::addressForByteOffset(inlineCallFrame->returnPCOffset()));
+ if (!Options::allowNonSPTagging()) {
+ JIT_COMMENT(jit, "lldb dynamic execution / posix signals could trash your stack"); // We don't have to worry about signals because they shouldn't fire in WebContent process in this window.
+ jit.move(MacroAssembler::stackPointerRegister, GPRInfo::regT4);
+ }
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: A maliciously crafted webpage may be able to fingerprint the user
Researchers: Emilio Cobos of Mozilla
Key insight. paintDecorations looked up the visited-dependent BACKGROUND color without honoring the paint behavior, so filters could read visited state through the background; passing paintBehavior makes the painter ignore visited styles where the result is observable (mirroring the earlier text-color fix).
Patch Details
diff --git a/LayoutTests/swipe/resources/swipe-test.js b/LayoutTests/swipe/resources/swipe-test.js
index a32ba832493c..132f6c9c6c7f 100644
--- a/LayoutTests/swipe/resources/swipe-test.js
+++ b/LayoutTests/swipe/resources/swipe-test.js
@@ -46,6 +46,8 @@ async function startSlowSwipeGesture()
if (!window.eventSender)
return;
+ log("startSlowSwipeGesture");
+
await UIHelper.ensurePresentationUpdate();
// Similar to uiController.beginBackSwipe(), but with a gap between events to allow
diff --git a/LayoutTests/swipe/swipe-back-with-active-wheel-listener-expected.txt b/LayoutTests/swipe/swipe-back-with-active-wheel-listener-expected.txt
index 8215851db5ff..fd5b55d8f959 100644
--- a/LayoutTests/swipe/swipe-back-with-active-wheel-listener-expected.txt
+++ b/LayoutTests/swipe/swipe-back-with-active-wheel-listener-expected.txt
@@ -1,4 +1,5 @@
Swipe target
+startSlowSwipeGesture
didBeginSwipe
completeSwipeGesture
willEndSwipe
diff --git a/LayoutTests/swipe/swipe-back-with-passive-wheel-listener-expected.txt b/LayoutTests/swipe/swipe-back-with-passive-wheel-listener-expected.txt
index 6bd807db86af..fd5b55d8f959 100644
--- a/LayoutTests/swipe/swipe-back-with-passive-wheel-listener-expected.txt
+++ b/LayoutTests/swipe/swipe-back-with-passive-wheel-listener-expected.txt
@@ -1,5 +1,5 @@
Swipe target
-startSwipeGesture
+startSlowSwipeGesture
didBeginSwipe
completeSwipeGesture
willEndSwipe
diff --git a/LayoutTests/swipe/wheel-prevent-default-prevents-swipe-back-expected.txt b/LayoutTests/swipe/wheel-prevent-default-prevents-swipe-back-expected.txt
index e7a0ac32652b..975b6f62289c 100644
--- a/LayoutTests/swipe/wheel-prevent-default-prevents-swipe-back-expected.txt
+++ b/LayoutTests/swipe/wheel-prevent-default-prevents-swipe-back-expected.txt
@@ -1,3 +1,4 @@
Swipe target
+startSlowSwipeGesture
completeSwipeGesture
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: A maliciously crafted webpage may be able to fingerprint the user
Researchers: an anonymous researcher
Key insight. Fixed, uniform audio noise gated by a single flag could be cancelled by looping one sample and averaging; making the noise normally-distributed, deterministic per repeated value, and loop-count-amplified for AudioBufferSourceNode defeats the averaging attack.
Patch Details
diff --git a/Source/WebKit/ChangeLog b/Source/WebKit/ChangeLog
index 30328cf6e845..c83f5658a6fb 100644
--- a/Source/WebKit/ChangeLog
+++ b/Source/WebKit/ChangeLog
@@ -1,3 +1,13 @@
+2020-12-14 Carlos Garcia Campos <[email protected]>
+
+ [WPE][GTK] Should enable WebProcessCache
+ https://bugs.webkit.org/show_bug.cgi?id=219689
+
+ Reviewed by Adrian Perez de Castro.
+
+ * UIProcess/API/glib/WebKitWebContext.cpp:
+ (webkitWebContextConstructed):
+
2020-12-13 Yusuke Suzuki <[email protected]>
[JSC] Introduce vmEntryCustomAccessor and vmEntryHostFunction for JITCage
diff --git a/Source/WebKit/UIProcess/API/glib/WebKitWebContext.cpp b/Source/WebKit/UIProcess/API/glib/WebKitWebContext.cpp
index f7faa5880bf0..acf9ccdb6d12 100644
--- a/Source/WebKit/UIProcess/API/glib/WebKitWebContext.cpp
+++ b/Source/WebKit/UIProcess/API/glib/WebKitWebContext.cpp
@@ -391,6 +391,7 @@ static void webkitWebContextConstructed(GObject* object)
API::ProcessPoolConfiguration configuration;
configuration.setInjectedBundlePath(FileSystem::stringFromFileSystemRepresentation(bundleFilename.get()));
+ configuration.setUsesWebProcessCache(true);
#if PLATFORM(GTK)
configuration.setProcessSwapsOnNavigation(priv->psonEnabled);
#if !USE(GTK4)
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to arbitrary code execution
Researchers: Nan Wang (@eternalsakura13) of 360 Vulnerability Research Institute
Key insight. Wasm call inlining hardcoded the inlinee as having no exception handlers and emitted invalid call-site indices, so an exception thrown through an inlined EH-bearing function dispatched on wrong metadata; propagating the real hasExceptionHandlers state and valid call-site indices fixes it.
Patch Details
diff --git a/JSTests/wasm/stress/inlinee-may-have-exception-handlers.js b/JSTests/wasm/stress/inlinee-may-have-exception-handlers.js
new file mode 100644
index 000000000000..686066ec9274
--- /dev/null
+++ b/JSTests/wasm/stress/inlinee-may-have-exception-handlers.js
@@ -0,0 +1,84 @@
+import * as assert from '../assert.js';
+import { instantiate } from "../wabt-wrapper.js";
+
+let wat = `
+(module
+ (func $main (export "main")
+ call $catcher
+ i64.const 3127057505886423800
+ i64.const -5049743701649469475
+ i32.const 1279394412
+ i32.const 1249280136
+ i32.const -851957055
+ i32.div_s
+ i32.div_s
+ select (result i64)
+ loop (result i64)
+ i64.const 7052281334997434446
+ f64.const 0x1.f0094d7063744p+967
+ i64.trunc_sat_f64_u
+ i64.rem_s
+ end
+ i64.add
+ f64.convert_i64_s
+ i32.const 993640798
+ i32.const -291103156
+ i32.atomic.rmw.xor offset=36014
+ f32.const 0x1.2e5784p-38
+ i64.const 8981315711995315489
+ i64.const 5932917051412947299
+ i64.const 6207621187208520631
+ i32.const 1636976966
+ select (result i64)
+ i64.ne
+ br 0
+ i32.trunc_sat_f32_u
+ i32.div_s
+ f64.const 0x1.7c35ecf56d865p-116
+ i32.const 1987656988
+ i64.const -1947434429939530388
+ f64.const -0x1.ca05a27b00c85p+372
+ i64.const 5169929820610505455
+ block (param f64 i32 f64 i32 i64 f64 i64)
+ drop
+ drop
+ drop
+ drop
+ drop
+ drop
+ drop
+ end
+ )
+ (func $empty (param i32))
+ (func $catcher (type 0)
+ try
+ i32.const 1
+ call $empty
+ catch_all
+ throw $exc
+ end
+ call $main
+ )
+ (memory 32 64)
+ (tag $exc)
+)
+`;
+
+async function test() {
+ let instance = await instantiate(wat, {}, {threads: true, exceptions: true});
+
+ let caughtCount = 0;
+ for (let i = 0; i < 10; i ++) {
+ try {
+ instance.exports.main();
+ } catch (e) {
+ // We expect to either overflow the stack, or end up throwing $exc back into JavaScript.
+ assert.truthy(e instanceof RangeError || e instanceof WebAssembly.Exception);
+ caughtCount ++;
+ }
+ }
+
+ assert.eq(caughtCount, 10);
+}
+
+assert.asyncTest(test());
diff --git a/JSTests/wasm/stress/rethrow-should-set-callsite-index.js b/JSTests/wasm/stress/rethrow-should-set-callsite-index.js
new file mode 100644
index 000000000000..07519a62e491
--- /dev/null
+++ b/JSTests/wasm/stress/rethrow-should-set-callsite-index.js
@@ -0,0 +1,40 @@
+import * as assert from '../assert.js';
+import { instantiate } from "../wabt-wrapper.js";
+
+let wat = `
+(module
+ (type (func))
+ (tag $exc (type 0))
+ (func $empty)
+ (func $rethrower
+ try
+ call $empty
+ throw $exc
+ catch_all
+ rethrow 0
+ end
+ )
+ (func $call-rethrower
+ call $rethrower
+ )
+ (export "call_rethrower" (func $call-rethrower))
+)
+`;
+
+async function test() {
+ let instance = await instantiate(wat, {}, {exceptions: true});
+
+ let caughtCount = 0;
+ for (let i = 0; i < 10000; i ++) {
+ try {
+ instance.exports.call_rethrower();
+ } catch (e) {
+ assert.instanceof(e, WebAssembly.Exception);
+ caughtCount ++;
+ }
+ }
+
+ assert.eq(caughtCount, 10000);
+}
+
+assert.asyncTest(test());
diff --git a/JSTests/wasm/stress/throw-should-set-callsite-index.js b/JSTests/wasm/stress/throw-should-set-callsite-index.js
new file mode 100644
index 000000000000..ee752f63010b
--- /dev/null
+++ b/JSTests/wasm/stress/throw-should-set-callsite-index.js
@@ -0,0 +1,39 @@
+import * as assert from '../assert.js';
+import { instantiate } from "../wabt-wrapper.js";
+
+let wat = `
+(module
+ (type (func))
+ (tag $exc (type 0))
+ (func $empty)
+ (func $thrower
+ try
+ call $empty
+ catch_all
+ end
+ throw $exc
+ )
+ (func $call-thrower
+ call $thrower
+ )
+ (export "call_thrower" (func $call-thrower))
+)
+`;
+
+async function test() {
+ let instance = await instantiate(wat, {}, {exceptions: true});
+
+ let caughtCount = 0;
+ for (let i = 0; i < 10000; i ++) {
+ try {
+ instance.exports.call_thrower();
+ } catch (e) {
+ assert.instanceof(e, WebAssembly.Exception);
+ caughtCount ++;
+ }
+ }
+
+ assert.eq(caughtCount, 10000);
+}
+
+assert.asyncTest(test());
diff --git a/Source/JavaScriptCore/wasm/WasmB3IRGenerator.cpp b/Source/JavaScriptCore/wasm/WasmB3IRGenerator.cpp
index 7f1d5feadd84..9ceede266fb0 100644
--- a/Source/JavaScriptCore/wasm/WasmB3IRGenerator.cpp
+++ b/Source/JavaScriptCore/wasm/WasmB3IRGenerator.cpp
@@ -345,8 +345,8 @@ public:
return m_callSiteIndex;
}
- B3IRGenerator(const ModuleInformation&, OptimizingJITCallee&, Procedure&, Vector<UnlinkedWasmToWasmCall>&, unsigned& osrEntryScratchBufferSize, MemoryMode, CompilationMode, unsigned functionIndex, std::optional<bool> hasExceptionHandlers, unsigned loopIndexForOSREntry, TierUpCount*);
- B3IRGenerator(B3IRGenerator& inlineCaller, B3IRGenerator& inlineRoot, unsigned functionIndex, BasicBlock* returnContinuation, Vector<Value*> args);
+ B3IRGenerator(CalleeGroup&, const ModuleInformation&, OptimizingJITCallee&, Procedure&, Vector<UnlinkedWasmToWasmCall>&, unsigned& osrEntryScratchBufferSize, MemoryMode, CompilationMode, unsigned functionIndex, std::optional<bool> hasExceptionHandlers, unsigned loopIndexForOSREntry, TierUpCount*);
+ B3IRGenerator(B3IRGenerator& inlineCaller, B3IRGenerator& inlineRoot, CalleeGroup&, unsigned functionIndex, std::optional<bool> hasExceptionHandlers, BasicBlock* returnContinuation, Vector<Value*> args);
void computeStackCheckSize(bool& needsOverflowCheck, int32_t& checkSize);
@@ -899,6 +899,7 @@ private:
void traceCF(Args&&... info);
FunctionParser<B3IRGenerator>* m_parser { nullptr };
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing a file may lead to unexpected app termination or arbitrary code execution
Researchers: Maksymilian Motyl of Immunity Systems, Junsung Lee working with Trend Micro Zero Day Initiative, and ajajfxhj
Key insight. The insertion reference node (nextChild) was a raw pointer held across removeChild and insertion, both of which run author script that can free it; keeping it as a RefPtr closes the mutate-during-insert use-after-free.
Patch Details
diff --git a/LayoutTests/fast/dom/set-attribute-and-normalize-in-event-expected.txt b/LayoutTests/fast/dom/set-attribute-and-normalize-in-event-expected.txt
new file mode 100644
index 000000000000..74c1f9350acf
--- /dev/null
+++ b/LayoutTests/fast/dom/set-attribute-and-normalize-in-event-expected.txt
@@ -0,0 +1,3 @@
+CONSOLE MESSAGE: RangeError: Maximum call stack size exceeded.
+CONSOLE MESSAGE: RangeError: Maximum call stack size exceeded.
+
diff --git a/LayoutTests/fast/dom/set-attribute-and-normalize-in-event.html b/LayoutTests/fast/dom/set-attribute-and-normalize-in-event.html
new file mode 100644
index 000000000000..5ec7cdccafa5
--- /dev/null
+++ b/LayoutTests/fast/dom/set-attribute-and-normalize-in-event.html
@@ -0,0 +1,17 @@
+<script>
+ function runTest() {
+ if (window.testRunner)
+ window.testRunner.dumpAsText();
+
+ marqueeElement.addEventListener("DOMSubtreeModified", () => {
+ try { hrElement.before(hrElement); } catch (e) { }
+ marqueeElement.normalize();
+ });
+
+ marqueeElement.setAttribute("a", "");
+ }
+</script>
+
+<body onload=runTest()>
+ <marquee id="marqueeElement">
+ <hr id="hrElement" width="1"></hr>
diff --git a/Source/WebCore/dom/ContainerNode.cpp b/Source/WebCore/dom/ContainerNode.cpp
index 30c7e9e66ba8..d3372f8d09d1 100644
--- a/Source/WebCore/dom/ContainerNode.cpp
+++ b/Source/WebCore/dom/ContainerNode.cpp
@@ -890,10 +890,11 @@ ExceptionOr<void> ContainerNode::appendChildWithoutPreInsertionValidityCheck(Nod
ExceptionOr<void> ContainerNode::insertChildrenBeforeWithoutPreInsertionValidityCheck(NodeVector&& newChildren, Node* nextChild)
{
+ RefPtr refChild = nextChild;
for (auto& child : newChildren) {
if (RefPtr oldParent = child->parentNode()) {
- if (nextChild == child.ptr())
- nextChild = child->nextSibling();
+ if (refChild.get() == child.ptr())
+ refChild = child->nextSibling();
if (auto result = oldParent->removeChild(child); result.hasException())
return result.releaseException();
}
@@ -910,14 +911,14 @@ ExceptionOr<void> ContainerNode::insertChildrenBeforeWithoutPreInsertionValidity
ChildListMutationScope mutation(*this);
for (auto& child : newChildren) {
- if (nextChild && nextChild->parentNode() != this) // Event listeners moved nextChild elsewhere.
+ if (refChild && refChild->parentNode() != this) // Event listeners moved nextChild elsewhere.
break;
if (child->parentNode()) // Event listeners inserted this child elsewhere.
break;
- executeNodeInsertionWithScriptAssertion(*this, child.get(), nextChild, ChildChange::Source::API, ReplacedAllChildren::No, [&] {
+ executeNodeInsertionWithScriptAssertion(*this, child.get(), refChild.get(), ChildChange::Source::API, ReplacedAllChildren::No, [&] {
child->setTreeScopeRecursively(treeScope());
- if (nextChild)
- insertBeforeCommon(*nextChild, child.get());
+ if (refChild)
+ insertBeforeCommon(*refChild, child.get());
else
appendChildCommon(child);
});
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may prevent Content Security Policy from being enforced
Researchers: Johan Carlsson (joaxcar)
Key insight. CSP was recomputed per multipart part and an absent header on a later part reset the policy to null; gating the clear on !m_isLoadingMultipartContent keeps a subsequent part from clearing the CSP established for the load.
Patch Details
diff --git a/Tools/Scripts/update-angle b/Tools/Scripts/update-angle
index ef380ec9a7f7..d8f402d3a9e8 100755
--- a/Tools/Scripts/update-angle
+++ b/Tools/Scripts/update-angle
@@ -2,6 +2,7 @@
set -e
cd "$(dirname "$0")/../../Source/ThirdParty/ANGLE"
ANGLE_DIR="$PWD"
+ANGLE_TARGET_COMMIT="origin/main"
regenerate_changes_diff() {
echo "Regenerating changes.diff."
@@ -42,11 +43,12 @@ regenerate_program_version_id() {
usage() {
SCRIPT_NAME=$(basename "$0")
- echo "USAGE: $SCRIPT_NAME [-h|--help] -[--regenerate-changes-diff[-main]]"
+ echo "USAGE: $SCRIPT_NAME [-h|--help] -[--regenerate-changes-diff[-main]] [commit]"
echo " -h | --help Print this help message."
echo " --regenerate-changes-diff Regenerate ANGLE changes.diff to last upstream merge."
echo " --regenerate-changes-diff-main Regenerate ANGLE changes.diff to upstream origin/main."
echo " --regenerate-program-version-id Regenerate ANGLE ANGLEShaderProgramVersion.h."
+ echo " commit The ANGLE commit to update to. Defaults to origin/main"
}
if [ ! -z "$1" ] ; then
@@ -90,10 +92,14 @@ if [ ! -z "$1" ] ; then
echo
echo "Success."
exit 0
+
+ elif [ "${1:0:2}" = -- ]; then
+ echo "ERROR: Unrecognized argument: $1"
+ usage
+ exit 1
+ else
+ ANGLE_TARGET_COMMIT="$1"
fi
- echo "ERROR: Unrecognized argument: $1"
- usage
- exit 1
fi
echo "This script helps you update the copy of ANGLE in Source/ThirdParty/ANGLE"
@@ -136,7 +142,7 @@ wait_for_rebase_to_complete() {
cleanup_after_successful_rebase_and_exit() {
cd "$ANGLE_DIR"
echo
- regenerate_changes_diff "origin/main"
+ regenerate_changes_diff "$ANGLE_TARGET_COMMIT"
git --no-pager diff -b --cached "$LAST_ROLL_COMMIT_HASH" -- Compiler.cmake GLESv2.cmake
echo
echo "Rebase complete!"
@@ -155,7 +161,7 @@ cleanup_after_successful_rebase_and_exit() {
echo "Press Enter to continue after fixing build:"
read -r
regenerate_program_version_id
- regenerate_changes_diff "origin/main"
+ regenerate_changes_diff "$ANGLE_TARGET_COMMIT"
echo "Generating contents of commit message into commit-message.txt."
echo "Be sure to copy out this file's contents and delete it before committing."
echo "Update ANGLE to $(git log -1 ${COMMIT_HASH} --format=%cs) (${COMMIT_HASH}))" > commit-message.txt
@@ -196,8 +202,9 @@ cd "$ANGLE_DIR"
echo "Downloading latest ANGLE via git clone."
# Remove all files including hidden ones, but not . or ..
rm -rf ..?* .[!.]* ./*
-git clone --branch main https://chromium.googlesource.com/angle/angle .
+git clone https://chromium.googlesource.com/angle/angle .
echo "Successfully downloaded latest ANGLE."
+git checkout -q "$ANGLE_TARGET_COMMIT"
echo "Commit hash: "
COMMIT_HASH=$(git rev-parse HEAD)
echo "$COMMIT_HASH"
@@ -222,7 +229,7 @@ sed -i.bak -e "s/<string>[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]<\/string>/<s
rm ANGLE.plist.bak
echo "Translating gni build files to cmake."
-git checkout origin/main -- src/compiler.gni src/libGLESv2.gni src/libANGLE/renderer/d3d/BUILD.gn
+git checkout "$ANGLE_TARGET_COMMIT" -- src/compiler.gni src/libGLESv2.gni src/libANGLE/renderer/d3d/BUILD.gn
./gni-to-cmake.py src/compiler.gni Compiler.cmake
./gni-to-cmake.py src/libGLESv2.gni GLESv2.cmake
./gni-to-cmake.py src/libANGLE/renderer/d3d/BUILD.gn D3D.cmake --prepend 'src/libANGLE/renderer/d3d/'
@@ -244,7 +251,7 @@ git replace --graft "$LAST_ROLL_COMMIT_HASH" "$PREVIOUS_ANGLE_COMMIT_HASH"
git checkout -b rebased-webkit-changes
echo "Rebasing WebKit's local changes on latest ANGLE main."
-if ! git rebase origin/main; then
+if ! git rebase "$ANGLE_TARGET_COMMIT"; then
echo
echo "There is now a temporary git repo in Source/ThirdParty/ANGLE with a"
echo "rebase in progress. You must resolve the merge conflict and continue"
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: A maliciously crafted webpage may be able to fingerprint the user
Researchers: an anonymous researcher
Key insight. DOMFileSystem resolved and stat’d paths even when it had no granted root (m_rootPath empty), so a File synthesized via DataTransfer could probe local filesystem metadata; guarding on a non-empty root closes the leak.
Patch Details
diff --git a/LayoutTests/http/tests/security/file-system-access-via-dataTransfer-expected.txt b/LayoutTests/http/tests/security/file-system-access-via-dataTransfer-expected.txt
new file mode 100644
index 000000000000..641ddaebab3e
--- /dev/null
+++ b/LayoutTests/http/tests/security/file-system-access-via-dataTransfer-expected.txt
@@ -0,0 +1,10 @@
+Test that accessing local file system metadata is not allowed
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+PASS Should not receive file
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/http/tests/security/file-system-access-via-dataTransfer.html b/LayoutTests/http/tests/security/file-system-access-via-dataTransfer.html
new file mode 100644
index 000000000000..caf05e1f85fc
--- /dev/null
+++ b/LayoutTests/http/tests/security/file-system-access-via-dataTransfer.html
@@ -0,0 +1,50 @@
+<html>
+<head>
+<script src="/js-test-resources/js-test.js"></script>
+<body>
+
+<script>
+description("Test that accessing local file system metadata is not allowed");
+
+function runTest() {
+ if (!window.internals) {
+ alert("This test depends on Internals");
+ return;
+ }
+
+ window.jsTestIsAsync = true;
+
+ let path = location.pathname.split("/");
+ let targetFileName = internals.createTemporaryFile(`${path[path.length - 1]}`, "");
+
+ let input = document.createElement("input");
+ input.type = "file";
+
+ let file = new File([], targetFileName, {"type":"text/plain"});
+
+ dataTransfer = new DataTransfer();
+ dataTransfer.items.add(file)
+ input.files = dataTransfer.files;
+
+ var functionOnSuccess = function (file)
+ {
+ testFailed("Should not receive file");
+ finishJSTest()
+ }
+
+ var functionOnError = function (value)
+ {
+ testPassed("Should not receive file");
+ finishJSTest()
+ }
+
+ input.webkitEntries.forEach((entry) => {
+ entry.file(functionOnSuccess, functionOnError)
+ });
+}
+
+runTest();
+
+</script>
+</body>
+</html>
diff --git a/Source/WebCore/Modules/entriesapi/DOMFileSystem.cpp b/Source/WebCore/Modules/entriesapi/DOMFileSystem.cpp
index 0c08ddbec609..e86b3dcc356c 100644
--- a/Source/WebCore/Modules/entriesapi/DOMFileSystem.cpp
+++ b/Source/WebCore/Modules/entriesapi/DOMFileSystem.cpp
@@ -303,6 +303,9 @@ void DOMFileSystem::getEntry(ScriptExecutionContext& context, FileSystemDirector
return;
}
+ if (m_rootPath.isEmpty())
+ return completionCallback(Exception { ExceptionCode::NotFoundError, "Path does not exist"_s });
+
m_workQueue->dispatch([protectedThis = Ref { *this }, context = Ref { context }, fullPath = crossThreadCopy(WTFMove(fullPath)), resolvedVirtualPath = crossThreadCopy(WTFMove(resolvedVirtualPath)), completionCallback = WTFMove(completionCallback)]() mutable {
auto entryType = fileTypeIgnoringHiddenFiles(fullPath);
callOnMainThread([protectedThis = WTFMove(protectedThis), context = WTFMove(context), resolvedVirtualPath = crossThreadCopy(WTFMove(resolvedVirtualPath)), entryType, completionCallback = WTFMove(completionCallback)]() mutable {
@@ -327,6 +330,8 @@ void DOMFileSystem::getEntry(ScriptExecutionContext& context, FileSystemDirector
void DOMFileSystem::getFile(ScriptExecutionContext& context, FileSystemFileEntry& fileEntry, GetFileCallback&& completionCallback)
{
+ if (m_rootPath.isEmpty())
+ return completionCallback(Exception { ExceptionCode::NotFoundError, "Path does not exist"_s });
auto virtualPath = fileEntry.virtualPath();
auto fullPath = evaluatePath(virtualPath);
m_workQueue->dispatch([fullPath = crossThreadCopy(WTFMove(fullPath)), virtualPath = crossThreadCopy(WTFMove(virtualPath)), context = Ref { context }, completionCallback = WTFMove(completionCallback)]() mutable {
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may prevent Content Security Policy from being enforced
Researchers: Georg Felber and Marco Squarcina
Key insight. Merging attributes from a duplicate body/html tag re-added the nonce onto an already-connected element without re-hiding it, so a value the spec requires to be invisible to attribute selectors became leakable; hiding (or discarding) the nonce during the merge restores the invariant.
Patch Details
diff --git a/LayoutTests/http/tests/security/referrer-policy-header.html b/LayoutTests/http/tests/security/referrer-policy-header.html
index 4bfe3dc27a22..39f7cacc7f70 100644
--- a/LayoutTests/http/tests/security/referrer-policy-header.html
+++ b/LayoutTests/http/tests/security/referrer-policy-header.html
@@ -8,9 +8,10 @@
<script>
description("Tests support for Referrer-Policy HTTP header.");
jsTestIsAsync = true;
-runTests(false);
-if (window.testRunner)
+if (window.testRunner) {
+ testRunner.waitUntilDone();
testRunner.setStatisticsShouldDowngradeReferrer(false, async () => { await runTests(false /* multipart */); });
+}
</script>
</body>
</html>
diff --git a/LayoutTests/platform/wk2/TestExpectations b/LayoutTests/platform/wk2/TestExpectations
index 10c1bdbcf4de..59832160e345 100644
--- a/LayoutTests/platform/wk2/TestExpectations
+++ b/LayoutTests/platform/wk2/TestExpectations
@@ -870,6 +870,4 @@ webkit.org/b/259409 imported/w3c/web-platform-tests/cookies/partitioned-cookies/
webkit.org/b/259482 fast/media/managed-media-source-open-crash.html [ Pass Failure ]
-webkit.org/b/260632 http/tests/security/referrer-policy-header.html [ Pass Failure Crash ]
-
webkit.org/b/260640 [ Release arm64 ] editing/execCommand/apply-inline-style-to-element-with-no-renderer-crash.html [ Pass Failure ]
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing web content may lead to a denial-of-service
Researchers: anbu1024 of SecANT
Background
Advisory: Apple Security Advisory
| Attribute | Value |
|---|---|
| CVE | CVE-2024-54658 |
| Bugzilla | 263758 |
| Component | WebCore |
| Bug Class | LogicError |
| Severity | medium |
| Advisory | Apple Advisory |
Fix not public. The WebKit fix for this bug is embargoed or not yet disclosed. The bug ID is confirmed from the Apple advisory, but no public commit references it.
Root Cause Analysis
The issue was addressed with improved memory handling.
Overview
Impact:
Description: A maliciously crafted webpage may be able to fingerprint the user
Researchers: an anonymous researcher
Key insight. The WebContent sandbox should deny access to stable device-identifying system files by default; an omitted deny rule is a fingerprinting side channel.
Patch Details
diff --git a/Source/WebKit/Resources/SandboxProfiles/ios/com.apple.WebKit.WebContent.sb.in b/Source/WebKit/Resources/SandboxProfiles/ios/com.apple.WebKit.WebContent.sb.in
index 9734cf26a015..a37b59016703 100644
--- a/Source/WebKit/Resources/SandboxProfiles/ios/com.apple.WebKit.WebContent.sb.in
+++ b/Source/WebKit/Resources/SandboxProfiles/ios/com.apple.WebKit.WebContent.sb.in
@@ -37,6 +37,9 @@
(allow process-info-codesignature)
#endif
+(deny file-read-metadata
+ (literal "/private/var/db/MobileIdentityData/Version.plist"))
+
;;;
;;; The following rules were originally contained in 'common.sb'. We are duplicating them here so we can
;;; remove unneeded sandbox extensions.
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing web content may lead to arbitrary code execution
Researchers: Wangtaiyu of Zhongfu info
Key insight. The asynchronous EME grant continuation held the Document by raw reference (and ran its completion handler unconditionally), so processing a grant after the frame was torn down dereferenced a freed context; WeakPtr/context null-checks and a run-once handler restore safe lifetime.
Patch Details
diff --git a/Source/WebKit/CMakeLists.txt b/Source/WebKit/CMakeLists.txt
index 3b782a4b3d6f..bd39f0753235 100644
--- a/Source/WebKit/CMakeLists.txt
+++ b/Source/WebKit/CMakeLists.txt
@@ -40,6 +40,7 @@ set(WebKit_PRIVATE_INCLUDE_DIRECTORIES
"${WEBKIT_DIR}/Shared/CoreIPCSupport"
"${WEBKIT_DIR}/Shared/Databases"
"${WEBKIT_DIR}/Shared/Databases/IndexedDB"
+ "${WEBKIT_DIR}/Shared/Extensions"
"${WEBKIT_DIR}/Shared/FileAPI"
"${WEBKIT_DIR}/Shared/Gamepad"
"${WEBKIT_DIR}/Shared/Notifications"
Background
Advisory: Apple Security Advisory
Overview
Impact:
Description: Processing maliciously crafted web content may lead to arbitrary code execution. Apple is aware of a report that this issue may have been exploited.
Researchers: An anonymous researcher
Key insight. DFG constant-folding trusted whatever property value the concurrent compiler observed, but only structures in the CheckStructure set were watched — a value folded from an unwatched intermediate structure during a live transition stayed valid after the object reached a checked structure. The fix folds only when a watchpoint guarantees the value cannot go stale.
Overview
Impact:
Description: A malicious website may cause unexpected cross-origin behavior
Researchers: James Lee (@Windowsrcer)
Key insight. Feature-policy enforcement special-cased iframe owners and silently skipped embed/object/frame owners; applying the default policy to any non-iframe owner closes the cross-origin capability gap.
Patch Details
diff --git a/LayoutTests/ChangeLog b/LayoutTests/ChangeLog
index 409133b0ed61..e0a3e7cb2553 100644
--- a/LayoutTests/ChangeLog
+++ b/LayoutTests/ChangeLog
@@ -1,3 +1,13 @@
+2020-08-18 Antti Koivisto <[email protected]>
+
+ The CSS specificity of :host() pseudo-classes is wrong
+ https://bugs.webkit.org/show_bug.cgi?id=202494
+ <rdar://problem/66292568>
+
+ Reviewed by Anders Carlsson.
+
+ * TestExpectations:
+
2020-08-18 Diego Pino Garcia <[email protected]>
[GTK] Unreviewed test gardening. Update test baseline after r265749.
diff --git a/LayoutTests/TestExpectations b/LayoutTests/TestExpectations
index e17f48539072..1d87e2921dd0 100644
--- a/LayoutTests/TestExpectations
+++ b/LayoutTests/TestExpectations
@@ -4420,7 +4420,6 @@ webkit.org/b/214461 imported/w3c/web-platform-tests/css/css-pseudo/spelling-erro
webkit.org/b/214462 imported/w3c/web-platform-tests/css/css-scoping/host-context-specificity-001.html [ ImageOnlyFailure ]
webkit.org/b/214462 imported/w3c/web-platform-tests/css/css-scoping/host-context-specificity-002.html [ ImageOnlyFailure ]
webkit.org/b/214462 imported/w3c/web-platform-tests/css/css-scoping/host-context-specificity-003.html [ ImageOnlyFailure ]
-webkit.org/b/214462 imported/w3c/web-platform-tests/css/css-scoping/host-specificity.html [ ImageOnlyFailure ]
webkit.org/b/214462 imported/w3c/web-platform-tests/css/css-scoping/host-with-default-namespace-001.html [ ImageOnlyFailure ]
webkit.org/b/214462 imported/w3c/web-platform-tests/css/css-scoping/shadow-directionality-001.tentative.html [ ImageOnlyFailure ]
webkit.org/b/214462 imported/w3c/web-platform-tests/css/css-scoping/shadow-directionality-002.tentative.html [ ImageOnlyFailure ]
diff --git a/Source/WebCore/ChangeLog b/Source/WebCore/ChangeLog
index 3433c7799ce4..383409fd4bc2 100644
--- a/Source/WebCore/ChangeLog
+++ b/Source/WebCore/ChangeLog
@@ -1,3 +1,18 @@
+2020-08-18 Antti Koivisto <[email protected]>
+
+ The CSS specificity of :host() pseudo-classes is wrong
+ https://bugs.webkit.org/show_bug.cgi?id=202494
+ <rdar://problem/66292568>
+
+ Reviewed by Anders Carlsson.
+
+ https://drafts.csswg.org/css-scoping/#host-selector
+
+ “The specificity of :host() is that of a pseudo-class, plus the specificity of its argument.”
+
+ * css/CSSSelector.cpp:
+ (WebCore::simpleSelectorSpecificityInternal):
+
2020-08-18 Youenn Fablet <[email protected]>
Add a JS built-in routine to mark a promise as handled
diff --git a/Source/WebCore/css/CSSSelector.cpp b/Source/WebCore/css/CSSSelector.cpp
index afd9bab0f9e6..c810b21d9c98 100644
--- a/Source/WebCore/css/CSSSelector.cpp
+++ b/Source/WebCore/css/CSSSelector.cpp
@@ -128,6 +128,7 @@ static unsigned simpleSelectorSpecificityInternal(const CSSSelector& simpleSelec
return 0;
case CSSSelector::PseudoClassNthChild:
case CSSSelector::PseudoClassNthLastChild:
+ case CSSSelector::PseudoClassHost:
return CSSSelector::addSpecificities(static_cast<unsigned>(SelectorSpecificityIncrement::ClassB), simpleSelector.selectorList() ? maxSpecificity(*simpleSelector.selectorList()) : 0);
default:
break;
Background
Advisory: Apple Security Advisory