Skip to content

Implement navigation rate limiter to prevent recursive navigation crashes#52691

Closed
roberto-apple wants to merge 1 commit intoWebKit:mainfrom
roberto-apple:eng/Prevent-ReadableStreamDefaultControllerFunction-from-crashing-on-stack-overflow
Closed

Implement navigation rate limiter to prevent recursive navigation crashes#52691
roberto-apple wants to merge 1 commit intoWebKit:mainfrom
roberto-apple:eng/Prevent-ReadableStreamDefaultControllerFunction-from-crashing-on-stack-overflow

Conversation

@roberto-apple
Copy link
Copy Markdown
Contributor

@roberto-apple roberto-apple commented Oct 20, 2025

b97666d

Implement navigation rate limiter to prevent recursive navigation crashes
https://bugs.webkit.org/show_bug.cgi?id=301144
rdar://161094454

Reviewed by NOBODY (OOPS!).

Recursive navigation.navigate() calls from event listeners can trigger stack overflow.
During resource cleanup for a ReadableStream, this causes an exception assertion in
ReadableStreamDefaultController. However, clearing the exception locally
simply reveals more downstream assertions, pointing to a need to prevent
stack overflow from occuring instead.

Add a sliding window rate limiter (100 navigations per 5 second window)
to prevent IPC flooding and stack overflow from recursive calls.
This matches Chromium's sustained rate (~20 navigations/second).

Tests: navigation-api/navigation-api-rate-limit-exceeded.html
       navigation-api/navigation-api-rate-limit-readable-stream-no-crash.html
       navigation-api/navigation-api-rate-limit-window-reset.html

* LayoutTests/navigation-api/navigation-api-rate-limit-exceeded-expected.txt: Added.
* LayoutTests/navigation-api/navigation-api-rate-limit-exceeded.html: Added.
* LayoutTests/navigation-api/navigation-api-rate-limit-readable-stream-no-crash-expected.txt: Added.
* LayoutTests/navigation-api/navigation-api-rate-limit-readable-stream-no-crash.html: Added.
* LayoutTests/navigation-api/navigation-api-rate-limit-window-reset-expected.txt: Added.
* LayoutTests/navigation-api/navigation-api-rate-limit-window-reset.html: Added.
* Source/WebCore/page/LocalDOMWindow.h:
* Source/WebCore/page/Navigation.cpp:
(WebCore::Navigation::innerDispatchNavigateEvent):
(WebCore::Navigation::RateLimiter::navigationAllowed):
* Source/WebCore/page/Navigation.h:
* Source/WebCore/testing/Internals.cpp:
(WebCore::Internals::setNavigationRateLimiterParameters):
(WebCore::Internals::resetNavigationRateLimiter):
* Source/WebCore/testing/Internals.h:
* Source/WebCore/testing/Internals.idl:

b97666d

Misc iOS, visionOS, tvOS & watchOS macOS Linux Windows Apple Internal
✅ 🧪 style ✅ 🛠 ios ✅ 🛠 mac ✅ 🛠 wpe ✅ 🛠 win ✅ 🛠 ios-apple
✅ 🧪 bindings ✅ 🛠 ios-sim ✅ 🛠 mac-AS-debug ✅ 🧪 wpe-wk2 ✅ 🧪 win-tests ✅ 🛠 mac-apple
✅ 🧪 webkitperl ✅ 🧪 ios-wk2 ✅ 🧪 api-mac ✅ 🧪 api-wpe ✅ 🛠 vision-apple
✅ 🧪 ios-wk2-wpt ✅ 🧪 mac-wk1 ✅ 🛠 wpe-cairo
✅ 🧪 api-ios ✅ 🧪 mac-wk2 ✅ 🛠 gtk
✅ 🛠 vision ❌ 🧪 mac-AS-debug-wk2 ✅ 🧪 gtk-wk2
✅ 🛠 vision-sim ✅ 🧪 mac-wk2-stress ✅ 🧪 api-gtk
✅ 🧪 vision-wk2 ✅ 🧪 mac-intel-wk2 ✅ 🛠 playstation
✅ 🛠 tv ✅ 🛠 mac-safer-cpp
✅ 🛠 tv-sim
✅ 🛠 watch
✅ 🛠 watch-sim

@roberto-apple roberto-apple self-assigned this Oct 20, 2025
@roberto-apple roberto-apple added the New Bugs Unclassified bugs are placed in this component until the correct component can be determined. label Oct 20, 2025
@webkit-ews-buildbot webkit-ews-buildbot added the merging-blocked Applied to prevent a change from being merged label Oct 21, 2025
@roberto-apple roberto-apple removed the merging-blocked Applied to prevent a change from being merged label Oct 21, 2025
@roberto-apple roberto-apple changed the title Prevent ReadableStreamDefaultControllerFunction from crashing on stack overflow Limit recursive calls to navigate Oct 21, 2025
@roberto-apple roberto-apple force-pushed the eng/Prevent-ReadableStreamDefaultControllerFunction-from-crashing-on-stack-overflow branch from e87e08f to f754826 Compare October 21, 2025 20:46
@roberto-apple roberto-apple marked this pull request as ready for review October 21, 2025 21:40
@roberto-apple roberto-apple requested a review from cdumez as a code owner October 21, 2025 21:40
@@ -230,6 +232,25 @@ class Navigation final : public RefCounted<Navigation>, public EventTarget, publ
RefPtr<NavigationAPIMethodTracker> m_upcomingNonTraverseMethodTracker;
HashMap<String, Ref<NavigationAPIMethodTracker>> m_upcomingTraverseMethodTrackers;
WeakHashSet<AbortHandler> m_abortHandlers;

static constexpr int maxNavigateCalls { 64 };
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Arbitrary constant for now

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how this number is practical.

@webkit-ews-buildbot
Copy link
Copy Markdown
Collaborator

Safer C++ Build #60121 (f754826)

❌ Found 1 failing file with 1 issue. Please address these issues before landing. See WebKit Guidelines for Safer C++ Programming.
(cc @rniwa)

Copy link
Copy Markdown
Contributor

@basuke basuke left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As we talked offline, we should align the Chrome behavior. But crashing easily like this is not the great situation.

@@ -230,6 +232,25 @@ class Navigation final : public RefCounted<Navigation>, public EventTarget, publ
RefPtr<NavigationAPIMethodTracker> m_upcomingNonTraverseMethodTracker;
HashMap<String, Ref<NavigationAPIMethodTracker>> m_upcomingTraverseMethodTrackers;
WeakHashSet<AbortHandler> m_abortHandlers;

static constexpr int maxNavigateCalls { 64 };
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how this number is practical.

@roberto-apple roberto-apple force-pushed the eng/Prevent-ReadableStreamDefaultControllerFunction-from-crashing-on-stack-overflow branch from f754826 to 7882240 Compare October 26, 2025 15:02
@webkit-ews-buildbot
Copy link
Copy Markdown
Collaborator

Safer C++ Build #62842 (7882240)

❌ Found 2 failing files with 3 issues. Please address these issues before landing. See WebKit Guidelines for Safer C++ Programming.
(cc @rniwa)

@webkit-ews-buildbot webkit-ews-buildbot added the merging-blocked Applied to prevent a change from being merged label Oct 26, 2025
@roberto-apple roberto-apple removed the merging-blocked Applied to prevent a change from being merged label Oct 26, 2025
@roberto-apple roberto-apple force-pushed the eng/Prevent-ReadableStreamDefaultControllerFunction-from-crashing-on-stack-overflow branch from 7882240 to 672f5e4 Compare October 26, 2025 16:47
@webkit-ews-buildbot webkit-ews-buildbot added the merging-blocked Applied to prevent a change from being merged label Oct 26, 2025
@roberto-apple roberto-apple removed the merging-blocked Applied to prevent a change from being merged label Oct 27, 2025
@roberto-apple roberto-apple force-pushed the eng/Prevent-ReadableStreamDefaultControllerFunction-from-crashing-on-stack-overflow branch from 672f5e4 to 5f65d5c Compare October 27, 2025 18:41
@webkit-ews-buildbot
Copy link
Copy Markdown
Collaborator

Safer C++ Build #63669 (5f65d5c)

❌ Found 1 failing file with 1 issue. Please address these issues before landing. See WebKit Guidelines for Safer C++ Programming.
(cc @rniwa)

@RupinMittal
Copy link
Copy Markdown
Contributor

Hey, thanks for looking into this! Have we checked what Chrome does about this problem? Do they also limit the number of calls to a certain number (if so, what number)? Or do they have a different solution?

@roberto-apple roberto-apple force-pushed the eng/Prevent-ReadableStreamDefaultControllerFunction-from-crashing-on-stack-overflow branch from 5f65d5c to 610f1dc Compare October 28, 2025 04:33
@roberto-apple
Copy link
Copy Markdown
Contributor Author

@RupinMittal I'm not aware of any explicit cap by Chrome so the number chosen (currently 10) is entirely an implementation detail. On Chrome (using the layout test attached) the behavior is to navigate to about:blank#blocked from what I observed.

@roberto-apple roberto-apple requested a review from basuke October 28, 2025 17:10
@basuke
Copy link
Copy Markdown
Contributor

basuke commented Oct 28, 2025

I took a look into Chromium and they have a throttler for each frame that limit any sort of navigation 200 times per last 10 seconds. The best thing is to implement similar throttler or limiter.

third_party/blink/renderer/core/frame/navigation_rate_limiter.cc

Copy link
Copy Markdown
Contributor

@basuke basuke left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I wrote in the previous message, we should add navigation count limiter, not only for Navigation API.

result.finished.catch(checkAbortError);
});

const stream = new Blob().stream();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this unused stream?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It came from the original fuzzer test case that was supplied in the radar. Having this present was causing an assertion exception in ReadableStreamDefaultController once the navigation events started stack overflowing.

I'll move it to a separate test to be more explicit.

@@ -230,6 +234,23 @@ class Navigation final : public RefCounted<Navigation>, public EventTarget, publ
RefPtr<NavigationAPIMethodTracker> m_upcomingNonTraverseMethodTracker;
HashMap<String, Ref<NavigationAPIMethodTracker>> m_upcomingTraverseMethodTrackers;
WeakHashSet<AbortHandler> m_abortHandlers;

static constexpr int maxNavigateCalls { 10 };
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the number from Chromium, 200 seems the good number.

explicit NavigateCallCounter(Navigation& nav)
: m_navigation { nav }
{
m_navigation->m_navigateCalls++;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just incrementing and no decrement. This logic seems broken.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The decrement is in destructor in Navigation.cpp. Maybe I'll move the constructor there as well so that they will be next to each other.

@roberto-apple roberto-apple changed the title Limit recursive calls to navigate Implement navigation rate limiter to prevent recursive navigation crashes Nov 4, 2025
@roberto-apple roberto-apple force-pushed the eng/Prevent-ReadableStreamDefaultControllerFunction-from-crashing-on-stack-overflow branch from 610f1dc to 7b556d0 Compare November 4, 2025 22:08
@webkit-ews-buildbot
Copy link
Copy Markdown
Collaborator

Safer C++ Build #64975 (7b556d0)

❌ Found 1 failing file with 1 issue. Please address these issues before landing. See WebKit Guidelines for Safer C++ Programming.
(cc @rniwa)

@webkit-ews-buildbot webkit-ews-buildbot added the merging-blocked Applied to prevent a change from being merged label Nov 4, 2025
@roberto-apple roberto-apple removed the merging-blocked Applied to prevent a change from being merged label Nov 5, 2025
@roberto-apple roberto-apple force-pushed the eng/Prevent-ReadableStreamDefaultControllerFunction-from-crashing-on-stack-overflow branch from 7b556d0 to c25adfa Compare November 5, 2025 21:09
@roberto-apple roberto-apple force-pushed the eng/Prevent-ReadableStreamDefaultControllerFunction-from-crashing-on-stack-overflow branch from c25adfa to b0233a1 Compare November 5, 2025 23:34
@roberto-apple roberto-apple requested a review from basuke November 6, 2025 17:07
private:
// Sliding window rate limiter: allows 80 navigations per 4 second window (~20/sec sustained).
// Chromium uses 200 navigations per 10 seconds. Both prevent IPC flooding and stack overflow
// from recursive navigation patterns.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you change the number from chromium? Didn't that prevent the crash? Also please add the url of chromium project that is the reason of this decision.

@@ -195,6 +197,28 @@ class Navigation final : public RefCounted<Navigation>, public EventTarget, publ
static Vector<Ref<HistoryItem>> filterHistoryItemsForNavigationAPI(Vector<Ref<HistoryItem>>&&, HistoryItem&);

private:
// Rate limiter to prevent excessive navigation requests.
class NavigationRateLimiter {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add one of the allocation macro. Read FastMalloc.h for the detail.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most classes should use WTF_MAKE_TZONE_ALLOCATED(ClassName).

class NavigationRateLimiter {
public:
NavigationRateLimiter() = default;
NavigationRateLimiter(const Navigation&) = delete;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use WTF_MAKE_NONCOPYABLE and WTF_MAKE_NONMOVABLE macros.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These Navigation should be this class name, right? That's one of the reason we use macro.

@@ -195,6 +197,28 @@ class Navigation final : public RefCounted<Navigation>, public EventTarget, publ
static Vector<Ref<HistoryItem>> filterHistoryItemsForNavigationAPI(Vector<Ref<HistoryItem>>&&, HistoryItem&);

private:
// Rate limiter to prevent excessive navigation requests.
class NavigationRateLimiter {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The class name can be simply RateLimiter if you define this class as internal class.

@@ -8157,3 +8157,6 @@ imported/w3c/web-platform-tests/css/CSS2/visudet/replaced-elements-min-width-80.

imported/w3c/web-platform-tests/css/CSS2/stacking-context/opacity-affects-block-in-inline.html [ ImageOnlyFailure ]
imported/w3c/web-platform-tests/css/CSS2/stacking-context/zindex-affects-block-in-inline.html [ ImageOnlyFailure ]

# Navigation API rate limiting window reset test takes >10 seconds
navigation-api/navigation-api-rate-limit-window-reset.html [ Slow ]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't like this kind of slow test. We can add Internal method to change those numbers for testing and also force resetting the limiter.

@@ -1267,4 +1271,32 @@ Vector<Ref<HistoryItem>> Navigation::filterHistoryItemsForNavigationAPI(Vector<R
return filteredItems;
}

bool Navigation::NavigationRateLimiter::navigationAllowed(Navigation& navigation)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This argument only used to get Document from the navigation.window(). It's better to pass Document to the constructor of the RateLimiter.

@@ -400,6 +400,10 @@ Navigation::Result Navigation::reload(ReloadOptions&& options, Ref<DeferredPromi
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigation-navigate
Navigation::Result Navigation::navigate(const String& url, NavigateOptions&& options, Ref<DeferredPromise>&& committed, Ref<DeferredPromise>&& finished)
{
// Enforce rate limiting to prevent excessive navigation requests.
if (RefPtr navigation = this; navigation && !m_rateLimiter.navigationAllowed(*navigation))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems weird. We can simply check !m_rateLimiter.navigationAllowed() if we don't pass Navigation. More over, moving back the duty to send message to console from RateLimiter to this class, the limiter just don't care about reference counted objects. Let's move them here and add the method wasReported() and markReported() to manage that.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually this is not the only place that invokes NavigateEvent. i.e. history.push(), setting location.href, etc. Look at innerDispatchNavigateEvent() that all invocation of NavigateEvent uses.

@roberto-apple roberto-apple force-pushed the eng/Prevent-ReadableStreamDefaultControllerFunction-from-crashing-on-stack-overflow branch from b0233a1 to 3c2fd15 Compare November 8, 2025 05:06
@webkit-ews-buildbot webkit-ews-buildbot added the merging-blocked Applied to prevent a change from being merged label Nov 8, 2025
@webkit-ews-buildbot
Copy link
Copy Markdown
Collaborator

Safer C++ Build #65620 (3c2fd15)

❌ Found 1 failing file with 1 issue. Please address these issues before landing. See WebKit Guidelines for Safer C++ Programming.
(cc @rniwa)

…shes

https://bugs.webkit.org/show_bug.cgi?id=301144
rdar://161094454

Reviewed by NOBODY (OOPS!).

Recursive navigation.navigate() calls from event listeners can trigger stack overflow.
During resource cleanup for a ReadableStream, this causes an exception assertion in
ReadableStreamDefaultController. However, clearing the exception locally
simply reveals more downstream assertions, pointing to a need to prevent
stack overflow from occuring instead.

Add a sliding window rate limiter (100 navigations per 5 second window)
to prevent IPC flooding and stack overflow from recursive calls.
This matches Chromium's sustained rate (~20 navigations/second).

Tests: navigation-api/navigation-api-rate-limit-exceeded.html
       navigation-api/navigation-api-rate-limit-readable-stream-no-crash.html
       navigation-api/navigation-api-rate-limit-window-reset.html

* LayoutTests/navigation-api/navigation-api-rate-limit-exceeded-expected.txt: Added.
* LayoutTests/navigation-api/navigation-api-rate-limit-exceeded.html: Added.
* LayoutTests/navigation-api/navigation-api-rate-limit-readable-stream-no-crash-expected.txt: Added.
* LayoutTests/navigation-api/navigation-api-rate-limit-readable-stream-no-crash.html: Added.
* LayoutTests/navigation-api/navigation-api-rate-limit-window-reset-expected.txt: Added.
* LayoutTests/navigation-api/navigation-api-rate-limit-window-reset.html: Added.
* Source/WebCore/page/LocalDOMWindow.h:
* Source/WebCore/page/Navigation.cpp:
(WebCore::Navigation::innerDispatchNavigateEvent):
(WebCore::Navigation::RateLimiter::navigationAllowed):
* Source/WebCore/page/Navigation.h:
* Source/WebCore/testing/Internals.cpp:
(WebCore::Internals::setNavigationRateLimiterParameters):
(WebCore::Internals::resetNavigationRateLimiter):
* Source/WebCore/testing/Internals.h:
* Source/WebCore/testing/Internals.idl:
@roberto-apple roberto-apple removed the merging-blocked Applied to prevent a change from being merged label Nov 9, 2025
@roberto-apple roberto-apple force-pushed the eng/Prevent-ReadableStreamDefaultControllerFunction-from-crashing-on-stack-overflow branch from 3c2fd15 to b97666d Compare November 9, 2025 04:55
@webkit-ews-buildbot webkit-ews-buildbot added the merging-blocked Applied to prevent a change from being merged label Nov 9, 2025
@roberto-apple roberto-apple requested a review from basuke November 10, 2025 17:59
@roberto-apple
Copy link
Copy Markdown
Contributor Author

Closing PR as it is associated with a fuzzer bug that is now resolved.
The work for adding a navigation rate limiter will be addressed here: #53798

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merging-blocked Applied to prevent a change from being merged New Bugs Unclassified bugs are placed in this component until the correct component can be determined.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants