Created
May 6, 2026 09:48
-
-
Save RatoX/60d8b7d6c68a8fc4d1c220fd4a5c61bb to your computer and use it in GitHub Desktop.
sw_memory_leak_poc
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!doctype html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>Service Worker fetch memory repro</title> | |
| <style> | |
| :root { | |
| color-scheme: light dark; | |
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; | |
| } | |
| body { | |
| margin: 0; | |
| padding: 24px; | |
| } | |
| main { | |
| display: grid; | |
| gap: 16px; | |
| max-width: 820px; | |
| margin: 0 auto; | |
| } | |
| button { | |
| width: fit-content; | |
| min-height: 36px; | |
| padding: 0 14px; | |
| font: inherit; | |
| } | |
| textarea { | |
| width: 100%; | |
| min-height: 420px; | |
| box-sizing: border-box; | |
| padding: 12px; | |
| font: 13px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; | |
| resize: vertical; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <main> | |
| <button id="start" type="button" disabled>Start</button> | |
| <textarea id="log" readonly></textarea> | |
| </main> | |
| <script> | |
| const REQUESTS = 20000; | |
| const CONCURRENCY = 10; | |
| const PAYLOAD_BYTES = 4096; | |
| const startButton = document.getElementById('start'); | |
| const logOutput = document.getElementById('log'); | |
| function log(message, data = {}) { | |
| const suffix = Object.keys(data).length ? ` ${JSON.stringify(data)}` : ''; | |
| logOutput.value += `[${new Date().toISOString()}] ${message}${suffix}\n`; | |
| logOutput.scrollTop = logOutput.scrollHeight; | |
| } | |
| async function setupServiceWorker() { | |
| if (!('serviceWorker' in navigator)) { | |
| throw new Error('Service workers are not available'); | |
| } | |
| const registration = await navigator.serviceWorker.register('/sw.js'); | |
| log('Service worker registered', { scope: registration.scope }); | |
| await navigator.serviceWorker.ready; | |
| if (!navigator.serviceWorker.controller) { | |
| await new Promise(resolve => { | |
| const timeout = setTimeout(resolve, 1000); | |
| navigator.serviceWorker.addEventListener('controllerchange', () => { | |
| clearTimeout(timeout); | |
| resolve(); | |
| }, { once: true }); | |
| }); | |
| } | |
| if (!navigator.serviceWorker.controller) { | |
| log('Reloading so the service worker controls this page'); | |
| location.reload(); | |
| return; | |
| } | |
| log('Ready', { controller: navigator.serviceWorker.controller.scriptURL }); | |
| startButton.disabled = false; | |
| } | |
| async function runRequests() { | |
| startButton.disabled = true; | |
| const runId = `${Date.now()}-${Math.random().toString(16).slice(2)}`; | |
| let next = 0; | |
| let completed = 0; | |
| let failed = 0; | |
| const startedAt = performance.now(); | |
| log('Run started', { requests: REQUESTS, concurrency: CONCURRENCY, payloadBytes: PAYLOAD_BYTES, runId }); | |
| async function worker() { | |
| while (next < REQUESTS) { | |
| const requestId = next++; | |
| const url = `/payload?run=${runId}&i=${requestId}&size=${PAYLOAD_BYTES}&cache=${Math.random()}`; | |
| try { | |
| const response = await fetch(url, { cache: 'no-store' }); | |
| if (!response.ok) { | |
| throw new Error(`HTTP ${response.status}`); | |
| } | |
| await response.arrayBuffer(); | |
| completed += 1; | |
| } catch (error) { | |
| failed += 1; | |
| log('Request failed', { requestId, error: String(error) }); | |
| } | |
| if ((completed + failed) % 1000 === 0 || completed + failed === REQUESTS) { | |
| log('Progress', { completed, failed, total: completed + failed }); | |
| } | |
| } | |
| } | |
| await Promise.all(Array.from({ length: CONCURRENCY }, () => worker())); | |
| log('Run finished', { | |
| completed, | |
| failed, | |
| elapsedMs: Math.round(performance.now() - startedAt), | |
| }); | |
| startButton.disabled = false; | |
| } | |
| startButton.addEventListener('click', () => { | |
| runRequests().catch(error => { | |
| log('Run crashed', { error: String(error), stack: error?.stack }); | |
| startButton.disabled = false; | |
| }); | |
| }); | |
| setupServiceWorker().catch(error => { | |
| log('Setup failed', { error: String(error), stack: error?.stack }); | |
| }); | |
| </script> | |
| </body> | |
| </html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| import argparse | |
| import time | |
| from http import HTTPStatus | |
| from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer | |
| from pathlib import Path | |
| from urllib.parse import parse_qs, urlencode, urlparse | |
| ROOT = Path(__file__).resolve().parent | |
| WEB_ROOT = ROOT | |
| class HarnessHandler(BaseHTTPRequestHandler): | |
| server_version = "WKWebViewHarness/1.0" | |
| def do_GET(self): | |
| parsed = urlparse(self.path) | |
| path = parsed.path | |
| if path == "/": | |
| self.serve_file(WEB_ROOT / "index.html", "text/html; charset=utf-8") | |
| return | |
| if path == "/index.html": | |
| self.serve_file(WEB_ROOT / "index.html", "text/html; charset=utf-8") | |
| return | |
| if path == "/blank.html": | |
| self.serve_file(WEB_ROOT / "blank.html", "text/html; charset=utf-8") | |
| return | |
| if path == "/sw.js": | |
| self.serve_file( | |
| WEB_ROOT / "sw.js", | |
| "application/javascript; charset=utf-8", | |
| extra_headers={"Service-Worker-Allowed": "/"}, | |
| ) | |
| return | |
| if path == "/payload": | |
| self.serve_payload(parsed.query) | |
| return | |
| if path == "/redirect": | |
| self.serve_redirect(parsed.query) | |
| return | |
| if path == "/favicon.ico": | |
| self.send_response(HTTPStatus.NO_CONTENT) | |
| self.end_headers() | |
| return | |
| self.send_error(HTTPStatus.NOT_FOUND, "Not found") | |
| def log_message(self, fmt, *args): | |
| print(f"[server] {self.address_string()} - {fmt % args}") | |
| def serve_file(self, file_path, content_type, extra_headers=None): | |
| if not file_path.exists(): | |
| self.send_error(HTTPStatus.NOT_FOUND, "Not found") | |
| return | |
| content = file_path.read_bytes() | |
| self.send_response(HTTPStatus.OK) | |
| self.send_header("Content-Type", content_type) | |
| self.send_header("Content-Length", str(len(content))) | |
| self.send_header("Cache-Control", "no-store, max-age=0") | |
| self.send_header("Pragma", "no-cache") | |
| if extra_headers: | |
| for name, value in extra_headers.items(): | |
| self.send_header(name, value) | |
| self.end_headers() | |
| self.wfile.write(content) | |
| def serve_payload(self, query): | |
| params = parse_qs(query) | |
| size = parse_int(params, "size", 1024, minimum=0, maximum=8 * 1024 * 1024) | |
| delay_ms = parse_int(params, "delay", 0, minimum=0, maximum=30_000) | |
| if delay_ms > 0: | |
| time.sleep(delay_ms / 1000) | |
| body = make_body(size) | |
| self.send_response(HTTPStatus.OK) | |
| self.send_header("Content-Type", "application/octet-stream") | |
| self.send_header("Content-Length", str(len(body))) | |
| self.send_header("Cache-Control", "no-store, max-age=0") | |
| self.send_header("Pragma", "no-cache") | |
| self.end_headers() | |
| self.wfile.write(body) | |
| def serve_redirect(self, query): | |
| params = parse_qs(query) | |
| target = params.get("to", ["/"])[0] | |
| if not target.startswith("/"): | |
| target = "/" | |
| target_params = { | |
| key: values[-1] | |
| for key, values in params.items() | |
| if key not in {"to"} and values | |
| } | |
| separator = "&" if "?" in target else "?" | |
| location = target | |
| if target_params: | |
| location = f"{target}{separator}{urlencode(target_params)}" | |
| self.send_response(HTTPStatus.FOUND) | |
| self.send_header("Location", location) | |
| self.send_header("Cache-Control", "no-store, max-age=0") | |
| self.end_headers() | |
| def parse_int(params, key, default, minimum, maximum): | |
| raw = params.get(key, [str(default)])[0] | |
| try: | |
| value = int(raw) | |
| except ValueError: | |
| value = default | |
| return max(minimum, min(maximum, value)) | |
| def make_body(size): | |
| if size <= 0: | |
| return b"" | |
| seed = b"0123456789abcdef" | |
| repeats = (size // len(seed)) + 1 | |
| return (seed * repeats)[:size] | |
| def main(): | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument("--host", default="127.0.0.1") | |
| parser.add_argument("--port", type=int, default=8787) | |
| args = parser.parse_args() | |
| server = ThreadingHTTPServer((args.host, args.port), HarnessHandler) | |
| url = f"http://{args.host}:{args.port}/" | |
| print(f"[server] serving {ROOT}") | |
| print(f"[server] open {url}") | |
| server.serve_forever() | |
| if __name__ == "__main__": | |
| main() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| self.addEventListener('install', event => { | |
| event.waitUntil(self.skipWaiting()); | |
| }); | |
| self.addEventListener('activate', event => { | |
| event.waitUntil(self.clients.claim()); | |
| }); | |
| self.addEventListener('fetch', event => { | |
| event.respondWith(fetch(event.request)); | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment