Local Network Access: concurrent cross-origin subresource requests to the same private-IP host fail/hang because LocalNetworkPermissionPrompt/LoopbackNetworkPermissionPrompt return a non-unique notificationID
Categories
(Core :: DOM: Networking, defect)
Tracking
()
People
(Reporter: ville.aari, Unassigned)
References
Details
Steps to reproduce:
- Have a page at a public origin (e.g., https://example.com) that embeds a third-party script via <script src="https://internal-host/widget.js">, where internal-host resolves to a private IP (RFC1918 range).
- That script, on load, fires several concurrent subresource requests (additional JS chunks, CSS, fonts, API calls) back to the same private-IP host.
- Load the page in Firefox 151+ with default settings (no special LNA permission pre-granted for this origin pair).
Minimal reproduction: on any page whose top-level origin differs from a private-IP-resolving target host, open the console and run:
async function testConcurrentLNA(count = 5) {
const base = "https://<private-ip-resolving-host>/<some-small-endpoint>";
const results = new Array(count).fill("pending");
const start = performance.now();
const promises = Array.from({ length: count }, (_, i) => {
const url = ${base}?lna-test=${i}-${Date.now()};
return fetch(url, { cache: "no-store" })
.then((res) => { results[i] = resolved (${res.status}) after ${(performance.now() - start).toFixed(0)}ms; })
.catch((err) => { results[i] = rejected: ${err.message} after ${(performance.now() - start).toFixed(0)}ms; });
});
setTimeout(() => { console.log("--- Status at 3s ---"); results.forEach((r, i) => console.log(Request ${i}: ${r})); }, 3000);
await Promise.allSettled(promises);
console.log("--- Final status ---");
results.forEach((r, i) => console.log(Request ${i}: ${r}));
}
testConcurrentLNA(5);
Actual results:
Of 5 concurrent requests to the same trivial (small JSON) endpoint, only one resolved successfully — and notably slowly (~3.5s for what should be a near-instant response). The other 4 concurrent requests failed within 1-2s with NetworkError when attempting to fetch resource. Example output:
--- Status at 3s ---
Request 0: pending
Request 1: rejected: NetworkError when attempting to fetch resource. (...) after 1105ms
Request 2: rejected: NetworkError when attempting to fetch resource. (...) after 1105ms
Request 3: rejected: NetworkError when attempting to fetch resource. (...) after 2164ms
Request 4: rejected: NetworkError when attempting to fetch resource. (...) after 2164ms
--- Final status ---
Request 0: resolved (200) after 3467ms
Request 1: rejected: NetworkError when attempting to fetch resource. (...) after 1105ms
Request 2: rejected: NetworkError when attempting to fetch resource. (...) after 2164ms
Request 3: rejected: NetworkError when attempting to fetch resource. (...) after 2164ms
Request 4: rejected: NetworkError when attempting to fetch resource. (...) after 2164ms
Note: in this run the first-fired request succeeded rather than the last, and losers failed explicitly rather than hanging indefinitely — a close variant of the exact behavior described in bug 2033408 ("only the last prompt callback fires... [others] hang"), likely the same underlying notificationID collision manifesting slightly differently depending on timing/version.
Expected results:
Each concurrent request should be evaluated for LNA permission independently and correctly resolve based on the actual (cached or freshly-granted) permission state — none should be dropped/orphaned due to notification-ID collisions with unrelated concurrent requests.
Summary of root cause
browser/modules/PermissionUI.sys.mjs's base PermissionPrompt class documents (line ~319):
▎ "PopupNotification requires a unique ID to open the notification. You must return a unique ID string here..."
However, both LNA-related prompt subclasses violate this contract:
- LoopbackNetworkPermissionPrompt.notificationID (line 1250): return "loopback-network";
- LocalNetworkPermissionPrompt.notificationID (line 1565): return "local-network";
Both return a static, hardcoded string rather than anything unique per request/origin/tab. Since PopupNotifications tracks a single active notification per (notificationID, browser) pair, when multiple concurrent LNA-gated requests are in flight at once (e.g., a page loading several cross-origin subresources — script, CSS, font, JSON — from the same private-IP-resolving host simultaneously), each spins up its own prompt object, but they all collide on the same notificationID. A later request's prompt object appears to silently replace the earlier one's tracked notification, so when permission resolution eventually happens, only the most-recently-registered request's Allow()/Cancel() callback fires (per netwerk/base/LNAPermissionRequest.cpp). The other requests' callbacks never fire — their underlying network requests hang or eventually fail with NetworkError.
Comment 1•4 months ago
|
||
The Bugbug bot thinks this bug should belong to the 'Core::DOM: Networking' component, but is not confident enough to move the bug to that component.
Updated•4 months ago
|
Comment 2•4 months ago
|
||
This should be fixed with 2033408.
Description
•