Skip to content

Conversation

@DawningW
Copy link
Contributor

fetch->data will be allocated and freed in onprogress if fetch attribute EMSCRIPTEN_FETCH_STREAM_DATA is used, however it will still be freed in fetch_free() causing double free problem.

fetch->data will be allocated and freed in onprogress
if fetch attribute EMSCRIPTEN_FETCH_STREAM_DATA is used,
however it will still be freed in fetch_free() causing double free problem.
@sbc100 sbc100 requested a review from brendandahl November 24, 2025 16:33
@sbc100
Copy link
Collaborator

sbc100 commented Nov 24, 2025

Maybe we can run some of the browser tests with lsan in order catch this?

@DawningW
Copy link
Contributor Author

Maybe we can run some of the browser tests with lsan in order catch this?

OK, I've added a test called test_fetch_stream_abort. Without this fix, the test will fail. ASAN reports the double-free problem in the following output.

[16456:49308:1125/035153.702:INFO:CONSOLE:146] "Downloading.. 1.22% complete. Received chunk [0, 1636597[", source: http://localhost:8888/test.html (146)
[16456:49308:1125/035153.706:INFO:CONSOLE:146] "Abort fetch when downloading", source: http://localhost:8888/test.html (146)
[16456:49308:1125/035153.707:INFO:CONSOLE:146] "Downloading failed with status code: 65535.", source: http://localhost:8888/test.html (146)
[16456:49308:1125/035153.708:INFO:CONSOLE:1431] "=================================================================", source: http://localhost:8888/test.js (1431)
[16456:49308:1125/035153.709:INFO:CONSOLE:1431] "==42==ERROR: AddressSanitizer: attempting double-free on 0x13e20800 in thread T0:", source: http://localhost:8888/test.js (1431)
[16456:49308:1125/035153.715:INFO:CONSOLE:1431] "    #0 0x0001a06c  (this.program+0x1a06c)", source: http://localhost:8888/test.js (1431)
[16456:49308:1125/035153.715:INFO:CONSOLE:1431] "    #1 0x00001f17  (this.program+0x1f17)", source: http://localhost:8888/test.js (1431)
[16456:49308:1125/035153.715:INFO:CONSOLE:1431] "    #2 0x00002052  (this.program+0x2052)", source: http://localhost:8888/test.js (1431)
[16456:49308:1125/035153.715:INFO:CONSOLE:1431] "    #3 0x00001c24  (this.program+0x1c24)", source: http://localhost:8888/test.js (1431)
[16456:49308:1125/035153.715:INFO:CONSOLE:1431] "    #4 0x00001b86  (this.program+0x1b86)", source: http://localhost:8888/test.js (1431)
[16456:49308:1125/035153.715:INFO:CONSOLE:1431] "    #5 0x800010ac  (JavaScript+0x10ac)", source: http://localhost:8888/test.js (1431)
[16456:49308:1125/035153.715:INFO:CONSOLE:1431] "    #6 0x8000108d in callUserCallback http://localhost:8888/test.js:4237:5", source: http://localhost:8888/test.js (1431)
[16456:49308:1125/035153.715:INFO:CONSOLE:1431] "0x13e20800 is located 0 bytes inside of 1636597-byte region [0x13e20800,0x13fb00f5)", source: http://localhost:8888/test.js (1431)
[16456:49308:1125/035153.717:INFO:CONSOLE:1431] "freed by thread T0 here:", source: http://localhost:8888/test.js (1431)
[16456:49308:1125/035153.718:INFO:CONSOLE:1431] "    #0 0x0001a06c  (this.program+0x1a06c)", source: http://localhost:8888/test.js (1431)
[16456:49308:1125/035153.718:INFO:CONSOLE:1431] "    #1 0x800002ae  (JavaScript+0x2ae)", source: http://localhost:8888/test.js (1431)
[16456:49308:1125/035153.719:INFO:CONSOLE:1431] "    #2 0x8000118c in xhr.onprogress http://localhost:8888/test.js:4492:5", source: http://localhost:8888/test.js (1431)
[16456:49308:1125/035153.719:INFO:CONSOLE:1431] "    #3 0x800012cc in FetchXHR.send http://localhost:8888/test.js:4812:16", source: http://localhost:8888/test.js (1431)
[16456:49308:1125/035153.719:INFO:CONSOLE:1431] "previously allocated by thread T0 here:", source: http://localhost:8888/test.js (1431)
[16456:49308:1125/035153.720:INFO:CONSOLE:1431] "    #0 0x0001a241  (this.program+0x1a241)", source: http://localhost:8888/test.js (1431)
[16456:49308:1125/035153.721:INFO:CONSOLE:1431] "    #1 0x800002ae  (JavaScript+0x2ae)", source: http://localhost:8888/test.js (1431)
[16456:49308:1125/035153.721:INFO:CONSOLE:1431] "    #2 0x8000117e in xhr.onprogress http://localhost:8888/test.js:4478:13", source: http://localhost:8888/test.js (1431)
[16456:49308:1125/035153.721:INFO:CONSOLE:1431] "    #3 0x800012cc in FetchXHR.send http://localhost:8888/test.js:4812:16", source: http://localhost:8888/test.js (1431)
[16456:49308:1125/035153.721:INFO:CONSOLE:1431] "SUMMARY: AddressSanitizer: double-free (this.program+0x1a068) ", source: http://localhost:8888/test.js (1431)
[16456:49308:1125/035153.721:INFO:CONSOLE:1431] "==42==ABORTING", source: http://localhost:8888/test.js (1431)
FAIL

After applying this fix, the test will pass.

@sbc100
Copy link
Collaborator

sbc100 commented Nov 24, 2025

Thanks for adding a test! @brendandahl WDYT?

emscripten_fetch_free(fetch->id);
fetch->id = 0;
free((void*)fetch->data);
if (!(fetch->__attributes.attributes & EMSCRIPTEN_FETCH_STREAM_DATA)) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

When EMSCRIPTEN_FETCH_STREAM_DATA is used where is ->data free'd? i.e. where is the double free exacly?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ah, I see the problem.

Can you instead set this pointer back to NULL when the data is free'd.

i.e. can you add {{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.data, 0, '*') }}} after the free call on line 671 of Fetch.js instead of this change?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm afraid not. Closing the fetch directly in onprogress callback will free fetch->data immediately. However, the ptr variable in xhr.onprogress remains non-zero even after setting fetch->data to NULL in fetch_free. This leads to a double free when ptr is freed at the end of xhr.onprogress.

emscripten/src/Fetch.js

Lines 642 to 672 in 797cf32

xhr.onprogress = (e) => {
// check if xhr was aborted by user and don't try to call back
if (!Fetch.xhrs.has(id)) {
return;
}
var ptrLen = (fetchAttrLoadToMemory && fetchAttrStreamData && xhr.response) ? xhr.response.byteLength : 0;
var ptr = 0;
if (ptrLen > 0 && fetchAttrLoadToMemory && fetchAttrStreamData) {
#if FETCH_DEBUG
dbg(`fetch: allocating ${ptrLen} bytes in Emscripten heap for xhr data`);
#endif
#if ASSERTIONS
assert(onprogress, 'When doing a streaming fetch, you should have an onprogress handler registered to receive the chunks!');
#endif
// Allocate byte data in Emscripten heap for the streamed memory block (freed immediately after onprogress call)
ptr = _malloc(ptrLen);
HEAPU8.set(new Uint8Array(/** @type{Array<number>} */(xhr.response)), ptr);
}
{{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.data, 'ptr', '*') }}}
writeI53ToI64(fetch + {{{ C_STRUCTS.emscripten_fetch_t.numBytes }}}, ptrLen);
writeI53ToI64(fetch + {{{ C_STRUCTS.emscripten_fetch_t.dataOffset }}}, e.loaded - ptrLen);
writeI53ToI64(fetch + {{{ C_STRUCTS.emscripten_fetch_t.totalBytes }}}, e.total);
{{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.readyState, 'xhr.readyState', 'i16') }}}
var status = xhr.status;
// If loading files from a source that does not give HTTP status code, assume success if we get data bytes
if (xhr.readyState >= 3 && xhr.status === 0 && e.loaded > 0) status = 200;
{{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.status, 'status', 'i16') }}}
if (xhr.statusText) stringToUTF8(xhr.statusText, fetch + {{{ C_STRUCTS.emscripten_fetch_t.statusText }}}, 64);
onprogress(fetch, e);
_free(ptr);
};

This problem can be reproduced by modifying the test to following code:

  attr.onprogress = [](emscripten_fetch_t *fetch) {
    printf("Abort fetch when downloading\n");
    emscripten_fetch_close(fetch);
  };

ASAN output:

[47964:34952:1125/232003.321:INFO:CONSOLE:146] "Downloading.. 0.58% complete. Received chunk [0, 785039[", source: http://localhost:8888/test.html (146)
[47964:34952:1125/232003.322:INFO:CONSOLE:146] "Abort fetch when downloading", source: http://localhost:8888/test.html (146)
[47964:34952:1125/232003.322:INFO:CONSOLE:146] "Downloading failed with status code: 65535.", source: http://localhost:8888/test.html (146)
[47964:34952:1125/232003.324:INFO:CONSOLE:1431] "=================================================================", source: http://localhost:8888/test.js (1431)
[47964:34952:1125/232003.324:INFO:CONSOLE:1431] "==42==ERROR: AddressSanitizer: attempting double-free on 0x13e20800 in thread T0:", source: http://localhost:8888/test.js (1431)
[47964:34952:1125/232003.327:INFO:CONSOLE:1431] "    #0 0x00019cc7  (this.program+0x19cc7)", source: http://localhost:8888/test.js (1431)
[47964:34952:1125/232003.327:INFO:CONSOLE:1431] "    #1 0x800002ae  (JavaScript+0x2ae)", source: http://localhost:8888/test.js (1431)
[47964:34952:1125/232003.327:INFO:CONSOLE:1431] "    #2 0x80001123 in xhr.onprogress http://localhost:8888/test.js:4387:5", source: http://localhost:8888/test.js (1431)
[47964:34952:1125/232003.327:INFO:CONSOLE:1431] "    #3 0x800012aa in FetchXHR.send http://localhost:8888/test.js:4778:16", source: http://localhost:8888/test.js (1431)
[47964:34952:1125/232003.327:INFO:CONSOLE:1431] "0x13e20800 is located 0 bytes inside of 785039-byte region [0x13e20800,0x13ee028f)", source: http://localhost:8888/test.js (1431)
[47964:34952:1125/232003.327:INFO:CONSOLE:1431] "freed by thread T0 here:", source: http://localhost:8888/test.js (1431)
[47964:34952:1125/232003.328:INFO:CONSOLE:1431] "    #0 0x00019cc7  (this.program+0x19cc7)", source: http://localhost:8888/test.js (1431)
[47964:34952:1125/232003.329:INFO:CONSOLE:1431] "    #1 0x00001b72  (this.program+0x1b72)", source: http://localhost:8888/test.js (1431)
[47964:34952:1125/232003.329:INFO:CONSOLE:1431] "    #2 0x00001cad  (this.program+0x1cad)", source: http://localhost:8888/test.js (1431)
[47964:34952:1125/232003.329:INFO:CONSOLE:1431] "    #3 0x0000187f  (this.program+0x187f)", source: http://localhost:8888/test.js (1431)
[47964:34952:1125/232003.329:INFO:CONSOLE:1431] "    #4 0x00001320  (this.program+0x1320)", source: http://localhost:8888/test.js (1431)
[47964:34952:1125/232003.329:INFO:CONSOLE:1431] "    #5 0x800012f9  (JavaScript+0x12f9)", source: http://localhost:8888/test.js (1431)
[47964:34952:1125/232003.329:INFO:CONSOLE:1431] "    #6 0x8000117e in callUserCallback http://localhost:8888/test.js:4478:5", source: http://localhost:8888/test.js (1431)
[47964:34952:1125/232003.329:INFO:CONSOLE:1431] "previously allocated by thread T0 here:", source: http://localhost:8888/test.js (1431)
[47964:34952:1125/232003.329:INFO:CONSOLE:1431] "    #0 0x00019e9c  (this.program+0x19e9c)", source: http://localhost:8888/test.js (1431)
[47964:34952:1125/232003.330:INFO:CONSOLE:1431] "    #1 0x800002ae  (JavaScript+0x2ae)", source: http://localhost:8888/test.js (1431)
[47964:34952:1125/232003.330:INFO:CONSOLE:1431] "    #2 0x80001115 in xhr.onprogress http://localhost:8888/test.js:4373:13", source: http://localhost:8888/test.js (1431)
[47964:34952:1125/232003.330:INFO:CONSOLE:1431] "    #3 0x800012aa in FetchXHR.send http://localhost:8888/test.js:4778:16", source: http://localhost:8888/test.js (1431)
[47964:34952:1125/232003.330:INFO:CONSOLE:1431] "SUMMARY: AddressSanitizer: double-free (this.program+0x19cc3) ", source: http://localhost:8888/test.js (1431)
[47964:34952:1125/232003.330:INFO:CONSOLE:1431] "==42==ABORTING", source: http://localhost:8888/test.js (1431)

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ok, that makes sense.

Howabout we switch to using realloc, avoiding the need to the inbetween free calls completely. Like this: #25863

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, I think it's better than my fix. It keeps the lifecycle of fetch->data consistent between streaming and non-streaming fetch requests.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Feel free the copy the changes from my PR into this one.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you!
Updated, and my newly added test test_fetch_stream_abort passed.

@sbc100 sbc100 merged commit e33466c into emscripten-core:main Nov 25, 2025
34 checks passed
@juj
Copy link
Collaborator

juj commented Nov 26, 2025

This PR added a new test browser.test_fetch_stream_abort_asan - I see that failing several times on my CI, e.g. http://clbri.com:8010/api/v2/logs/230269/raw_inline

Does not seem to occur 100% of the time.

@sbc100
Copy link
Collaborator

sbc100 commented Nov 26, 2025

This PR added a new test browser.test_fetch_stream_abort_asan - I see that failing several times on my CI, e.g. http://clbri.com:8010/api/v2/logs/230269/raw_inline

Does not seem to occur 100% of the time.

Looks like maybe something to do with the fact that the fetch is being cancelled .. the server looks like its throwing:

Exception occurred during processing of request from ('127.0.0.1', 64044)
Traceback (most recent call last):
  File "/Users/clb/buildbot/apple-m4-2024-16gb-tahoe-26_0/emscripten_mac26_0_arm64/build/python/3.13.3_64bit/lib/python3.13/socketserver.py", line 697, in process_request_thread
    self.finish_request(request, client_address)
    ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/clb/buildbot/apple-m4-2024-16gb-tahoe-26_0/emscripten_mac26_0_arm64/build/python/3.13.3_64bit/lib/python3.13/socketserver.py", line 362, in finish_request
    self.RequestHandlerClass(request, client_address, self)
    ~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/clb/buildbot/apple-m4-2024-16gb-tahoe-26_0/emscripten_mac26_0_arm64/build/python/3.13.3_64bit/lib/python3.13/http/server.py", line 672, in __init__
    super().__init__(*args, **kwargs)
    ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^
  File "/Users/clb/buildbot/apple-m4-2024-16gb-tahoe-26_0/emscripten_mac26_0_arm64/build/python/3.13.3_64bit/lib/python3.13/socketserver.py", line 766, in __init__
    self.handle()
    ~~~~~~~~~~~^^
  File "/Users/clb/buildbot/apple-m4-2024-16gb-tahoe-26_0/emscripten_mac26_0_arm64/build/python/3.13.3_64bit/lib/python3.13/http/server.py", line 436, in handle
    self.handle_one_request()
    ~~~~~~~~~~~~~~~~~~~~~~~^^
  File "/Users/clb/buildbot/apple-m4-2024-16gb-tahoe-26_0/emscripten_mac26_0_arm64/build/python/3.13.3_64bit/lib/python3.13/http/server.py", line 424, in handle_one_request
    method()
    ~~~~~~^^
  File "/Users/clb/buildbot/apple-m4-2024-16gb-tahoe-26_0/emscripten_mac26_0_arm64/build/emscripten/main/test/browser_common.py", line 508, in do_GET
    SimpleHTTPRequestHandler.do_GET(self)
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
  File "/Users/clb/buildbot/apple-m4-2024-16gb-tahoe-26_0/emscripten_mac26_0_arm64/build/python/3.13.3_64bit/lib/python3.13/http/server.py", line 679, in do_GET
    self.copyfile(f, self.wfile)
    ~~~~~~~~~~~~~^^^^^^^^^^^^^^^
  File "/Users/clb/buildbot/apple-m4-2024-16gb-tahoe-26_0/emscripten_mac26_0_arm64/build/python/3.13.3_64bit/lib/python3.13/http/server.py", line 878, in copyfile
    shutil.copyfileobj(source, outputfile)
    ~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^
  File "/Users/clb/buildbot/apple-m4-2024-16gb-tahoe-26_0/emscripten_mac26_0_arm64/build/python/3.13.3_64bit/lib/python3.13/shutil.py", line 204, in copyfileobj
    fdst_write(buf)
    ~~~~~~~~~~^^^^^
  File "/Users/clb/buildbot/apple-m4-2024-16gb-tahoe-26_0/emscripten_mac26_0_arm64/build/python/3.13.3_64bit/lib/python3.13/socketserver.py", line 845, in write
    self._sock.sendall(b)
    ~~~~~~~~~~~~~~~~~~^^^
BrokenPipeError: [Errno 32] Broken pipe

That doesn't explain why it would only be the asan version that failed though..

@sbc100
Copy link
Collaborator

sbc100 commented Nov 26, 2025

Hmm.. actually it looks like that exception is shown even on successful runs:

$ ./test/runner browser.test_fetch_stream_abort -v

----------------------------------------
Exception occurred during processing of request from ('127.0.0.1', 58850)
Traceback (most recent call last):
  File "/usr/lib/python3.13/socketserver.py", line 697, in process_request_thread
    self.finish_request(request, client_address)
    ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.13/socketserver.py", line 362, in finish_request
    self.RequestHandlerClass(request, client_address, self)
    ~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.13/http/server.py", line 672, in __init__
    super().__init__(*args, **kwargs)
    ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.13/socketserver.py", line 766, in __init__
    self.handle()
    ~~~~~~~~~~~^^
  File "/usr/lib/python3.13/http/server.py", line 436, in handle
    self.handle_one_request()
    ~~~~~~~~~~~~~~~~~~~~~~~^^
  File "/usr/lib/python3.13/http/server.py", line 424, in handle_one_request
    method()
    ~~~~~~^^
  File "/usr/local/google/home/sbc/dev/wasm/emscripten/test/browser_common.py", line 508, in do_GET
    SimpleHTTPRequestHandler.do_GET(self)
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
  File "/usr/lib/python3.13/http/server.py", line 679, in do_GET
    self.copyfile(f, self.wfile)
    ~~~~~~~~~~~~~^^^^^^^^^^^^^^^
  File "/usr/lib/python3.13/http/server.py", line 881, in copyfile
    shutil.copyfileobj(source, outputfile)
    ~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.13/shutil.py", line 204, in copyfileobj
    fdst_write(buf)
    ~~~~~~~~~~^^^^^
  File "/usr/lib/python3.13/socketserver.py", line 845, in write
    self._sock.sendall(b)
    ~~~~~~~~~~~~~~~~~~^^^
ConnectionResetError: [Errno 104] Connection reset by peer
----------------------------------------
ok

----------------------------------------------------------------------
Ran 1 test in 1.686s

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants