diff --git a/CHANGELOG.md b/CHANGELOG.md index 85b4665..c2f63c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Libcurl.js Changelog: +## v0.6.9 (4/30/24): +- Fix a possible double free when requests get aborted +- Handle `ReadableStream` objects as the request payload +- Support the Clang AddressSanitizer + ## v0.6.8 (4/30/24): - Add support for relative URLs in HTTP sessions - Better error handling for invalid URLs diff --git a/README.md b/README.md index ed28743..1cfb377 100644 --- a/README.md +++ b/README.md @@ -59,8 +59,11 @@ sudo apt install make cmake emscripten autoconf automake libtool pkg-config wget The build script will generate `client/out/libcurl.js` as well as `client/out/libcurl.mjs`, which is an ES6 module. You can supply the following arguments to the build script to control the build: - `release` - Use all optimizations. - `single_file` - Include the WASM binary in the outputted JS using base64. +- `asan` - Use the Clang AddressSanitizer to catch possible memory bugs - `all` - Build twice, once normally, and once as a single file. +Note: non-release builds will have the `-dev` version suffix and ASan builds will have the `-asan` suffix. + ## Javascript API: ### Importing the Library: diff --git a/client/build.sh b/client/build.sh index 1bc0b66..b26279f 100755 --- a/client/build.sh +++ b/client/build.sh @@ -28,7 +28,7 @@ EXPORTED_FUNCS="${EXPORTED_FUNCS:1}" #compile options RUNTIME_METHODS="addFunction,removeFunction,allocate,ALLOC_NORMAL" COMPILER_OPTIONS="-o $MODULE_FILE -lcurl -lwolfssl -lcjson -lz -lbrotlidec -lbrotlicommon -lnghttp2 -I $INCLUDE_DIR -L $LIB_DIR" -EMSCRIPTEN_OPTIONS="-lwebsocket.js -sENVIRONMENT=worker,web -sASSERTIONS=1 -sLLD_REPORT_UNDEFINED -sALLOW_TABLE_GROWTH -sALLOW_MEMORY_GROWTH -sEXPORTED_FUNCTIONS=$EXPORTED_FUNCS -sEXPORTED_RUNTIME_METHODS=$RUNTIME_METHODS" +EMSCRIPTEN_OPTIONS="-lwebsocket.js -sENVIRONMENT=worker,web -sASSERTIONS=1 -sLLD_REPORT_UNDEFINED -sALLOW_TABLE_GROWTH -sALLOW_MEMORY_GROWTH -sNO_EXIT_RUNTIME -sEXPORTED_FUNCTIONS=$EXPORTED_FUNCS -sEXPORTED_RUNTIME_METHODS=$RUNTIME_METHODS" #clean output dir rm -rf $OUT_DIR @@ -50,8 +50,13 @@ if [[ "$*" == *"release"* ]]; then COMPILER_OPTIONS="-Oz -flto $COMPILER_OPTIONS" echo "note: building with release optimizations" else - COMPILER_OPTIONS="$COMPILER_OPTIONS --profiling -g" - EMSCRIPTEN_OPTIONS="$EMSCRIPTEN_OPTIONS -sSTACK_OVERFLOW_CHECK=2 -sSAFE_HEAP=1" + COMPILER_OPTIONS="$COMPILER_OPTIONS --profiling -g " + EMSCRIPTEN_OPTIONS="$EMSCRIPTEN_OPTIONS -sSTACK_OVERFLOW_CHECK=2" +fi + +if [[ "$*" == *"asan"* ]]; then + COMPILER_OPTIONS="$COMPILER_OPTIONS -fsanitize=address" + echo "note: building with asan, performance will suffer" fi if [[ "$*" == *"single_file"* ]]; then @@ -78,6 +83,8 @@ rm $MODULE_FILE #add version number and copyright notice VERSION=$(cat package.json | jq -r '.version') +[[ "$*" != *"release"* ]] && VERSION="$VERSION-dev" +[[ "$*" == *"asan"* ]] && VERSION="$VERSION-asan" sed -i "s/__library_version__/$VERSION/" $OUT_FILE WISP_VERSION=$(cat $WISP_CLIENT/package.json | jq -r '.version') sed -i "s/__wisp_version__/$WISP_VERSION/" $OUT_FILE diff --git a/client/javascript/http.js b/client/javascript/http.js index ec4456b..b459613 100644 --- a/client/javascript/http.js +++ b/client/javascript/http.js @@ -41,9 +41,9 @@ class HTTPSession extends CurlSession { request_async(url, params, body) { return new Promise((resolve, reject) => { - let http_handle; + let http_handle = null; let body_ptr = null; - + let headers_callback = (stream) => { let response_json = c_func_str(_http_get_info, [http_handle]); let response = this.constructor.create_response(stream, JSON.parse(response_json)); @@ -59,6 +59,12 @@ class HTTPSession extends CurlSession { _free(body_ptr); body_ptr = null; } + if (http_handle == null) { + //a race condition with aborting requests may lead to this state + //if the request gets cancelled right before it finishes normally, this function gets called twice + //fortunately, we can just return here to prevent anything bad from happening + return; + } if (error > 0) { error_msg(`Request "${url}" failed with error code ${error}: ${get_error_str(error)}`); reject(`Request failed with error code ${error}: ${get_error_str(error)}`); @@ -70,6 +76,7 @@ class HTTPSession extends CurlSession { reject("Request failed because redirects were disallowed."); } this.remove_request(http_handle); + http_handle = null; } body_ptr = body ? allocate_array(body) : null; @@ -94,9 +101,15 @@ class HTTPSession extends CurlSession { params.headers = params.headers || Object.fromEntries(resource.headers); params.method = params.method || resource.method; } - else { + else if (typeof url === "string" || url instanceof String) { url = (new URL(url, this.base_url)).href; } + else if (url instanceof URL) { + url = url.href; + } + else { + url = "" + url; + } let body = await this.constructor.create_options(params); return await this.request_async(url, params, body); } diff --git a/client/javascript/session.js b/client/javascript/session.js index e3e0574..6259f79 100644 --- a/client/javascript/session.js +++ b/client/javascript/session.js @@ -166,6 +166,8 @@ class CurlSession { } catch (e) { //the readable stream has been closed elsewhere, so cancel the request + if (aborted) return; + aborted = true; if (e instanceof TypeError) { end_callback(-1); } diff --git a/client/libcurl/http.c b/client/libcurl/http.c index ace91c0..4821a13 100644 --- a/client/libcurl/http.c +++ b/client/libcurl/http.c @@ -110,4 +110,7 @@ char* http_get_info(CURL* http_handle) { cJSON_Delete(response_json); return response_json_str; -} \ No newline at end of file +} + +//the address sanitizer falsely flags any malloc operation as a memory leak +const char* __asan_default_options() { return "detect_leaks=false"; } \ No newline at end of file diff --git a/client/package.json b/client/package.json index 0a9269c..919fe24 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "libcurl.js", - "version": "0.6.8", + "version": "0.6.9", "description": "An experimental port of libcurl to WebAssembly for use in the browser.", "main": "libcurl.mjs", "exports": {