From f87e68487996f12d447974d0cefb85c3e956e885 Mon Sep 17 00:00:00 2001 From: Sadhbh Code Date: Tue, 2 Jun 2026 20:13:17 +0100 Subject: [PATCH 01/65] Added note test runner for wasix quickjs --- Makefile | 8 +++++++- scripts/edge-wasix-node-runner.sh | 34 +++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100755 scripts/edge-wasix-node-runner.sh diff --git a/Makefile b/Makefile index 58b89f89e..fff76355a 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: build build-edge build-edge-quickjs-cli build-wasix build-quickjs-wasix build-napi build-napi-quickjs build-native-v8 build-native-quickjs build-wasix-napi build-wasix-napi-quickjs build-napi-wasmer-cli test-wasix-napi test-wasix-napi-quickjs test-wasix-napi-cli test-wasix-safe-mode test test-only check-portability clean clean-napi-quickjs clean-edge-quickjs-cli clean-dist dist dist-only framework-test framework-test-reset +.PHONY: build build-edge build-edge-quickjs-cli build-wasix build-quickjs-wasix build-napi build-napi-quickjs build-native-v8 build-native-quickjs build-wasix-napi build-wasix-napi-quickjs build-napi-wasmer-cli test-wasix-napi test-wasix-napi-quickjs test-wasix-napi-cli test-wasix-safe-mode test-wasix-quickjs-only test test-only check-portability clean clean-napi-quickjs clean-edge-quickjs-cli clean-dist dist dist-only framework-test framework-test-reset UNAME_S := $(shell uname -s) UNAME_M := $(shell uname -m) @@ -30,6 +30,7 @@ WASIX_NAPI_SMOKE_JS ?= console.log('hello world!'); WASMER_BIN ?= wasmer WASIX_PACKAGE_DIR ?= $(CURDIR) WASIX_SSL_CERTS_DIR ?= ssl-certs +WASIX_QUICKJS_NODE_TEST_RUNNER ?= $(CURDIR)/scripts/edge-wasix-node-runner.sh EDGE_VERSION_MAJOR := $(shell awk '$$2 == "EDGE_MAJOR_VERSION" {print $$3; exit}' src/edge_version.h) EDGE_VERSION_MINOR := $(shell awk '$$2 == "EDGE_MINOR_VERSION" {print $$3; exit}' src/edge_version.h) EDGE_VERSION_PATCH := $(shell awk '$$2 == "EDGE_PATCH_VERSION" {print $$3; exit}' src/edge_version.h) @@ -191,6 +192,11 @@ test-quickjs-only: --skip-tests=$(QUICKJS_SKIP_TESTS) \ -j $(TEST_JOBS) +test-wasix-quickjs-only: + NODE_TEST_RUNNER=$(WASIX_QUICKJS_NODE_TEST_RUNNER) WASMER_BIN="$(WASMER_BIN)" EDGEJS_ROOT="$(CURDIR)" WASIX_EDGEJS_PACKAGE_DIR="$(CURDIR)/quickjs-wasm" ./test/nodejs_test_harness --category=node:buffer,node:console,node:dgram,node:diagnostics_channel,node:dns,node:events,node:http,node:https,node:os,node:path,node:punycode,node:querystring,node:stream,node:string_decoder,node:tty,node:url,node:zlib,node:crypto,node:domain,node:http2,node:tls,node:sys \ + --skip-tests=$(QUICKJS_SKIP_TESTS) \ + -j $(TEST_JOBS) + clean-edge-quickjs-cli: rm -rf $(BUILD_EDGE_QUICKJS_CLI_DIR) diff --git a/scripts/edge-wasix-node-runner.sh b/scripts/edge-wasix-node-runner.sh new file mode 100755 index 000000000..151340c45 --- /dev/null +++ b/scripts/edge-wasix-node-runner.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +default_edgejs_root="$(cd "${script_dir}/.." && pwd)" + +edgejs_root="${EDGEJS_ROOT:-${default_edgejs_root}}" +wasmer_bin="${WASMER_BIN:-wasmer}" +package_dir="${WASIX_EDGEJS_PACKAGE_DIR:-${edgejs_root}/quickjs-wasm}" +guest_root="${WASIX_EDGEJS_GUEST_ROOT:-/workspace}" + +guest_args=() +for arg in "$@"; do + case "${arg}" in + "${edgejs_root}"/*) + guest_args+=("${guest_root}/${arg#"${edgejs_root}/"}") + ;; + "${edgejs_root}") + guest_args+=("${guest_root}") + ;; + *) + guest_args+=("${arg}") + ;; + esac +done + +exec "${wasmer_bin}" run \ + --net \ + --env HOME=/tmp \ + --env "NODE_TEST_DIR=${guest_root}/test" \ + --volume "${edgejs_root}:${guest_root}" \ + --cwd "${guest_root}" \ + "${package_dir}" \ + -- "${guest_args[@]}" From 2baeceeaba641eb8e5ad61a88c1caf5b75a24547 Mon Sep 17 00:00:00 2001 From: Sadhbh Code Date: Tue, 2 Jun 2026 20:17:37 +0100 Subject: [PATCH 02/65] Added node test runner wasix quickjs to gh workflow --- .github/workflows/test-and-build-quickjs.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/test-and-build-quickjs.yml b/.github/workflows/test-and-build-quickjs.yml index e645402e1..3429b0d93 100644 --- a/.github/workflows/test-and-build-quickjs.yml +++ b/.github/workflows/test-and-build-quickjs.yml @@ -183,6 +183,15 @@ jobs: set -euo pipefail make build-quickjs-wasix + - name: Install Wasmer CLI + uses: wasmerio/setup-wasmer@v3 + + - name: Test edge (QuickJS WASIX) + shell: bash + run: | + set -euo pipefail + make test-wasix-quickjs-only + - name: Generate dist shell: bash run: | From 352c55f3cf2d2b1083c48eb4cf190b3b43859ce0 Mon Sep 17 00:00:00 2001 From: Sadhbh Code Date: Tue, 2 Jun 2026 20:57:25 +0100 Subject: [PATCH 03/65] Set wasmer version to 7.2.0-alpha.3 for wasix tests --- .github/workflows/test-and-build-quickjs.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test-and-build-quickjs.yml b/.github/workflows/test-and-build-quickjs.yml index 3429b0d93..5ee4ed9ad 100644 --- a/.github/workflows/test-and-build-quickjs.yml +++ b/.github/workflows/test-and-build-quickjs.yml @@ -185,6 +185,8 @@ jobs: - name: Install Wasmer CLI uses: wasmerio/setup-wasmer@v3 + with: + version: 7.2.0-alpha.3 - name: Test edge (QuickJS WASIX) shell: bash From c63d77715b7f0d96ac1ef027cfa885ffcae229e2 Mon Sep 17 00:00:00 2001 From: Sadhbh Code Date: Tue, 2 Jun 2026 21:31:41 +0100 Subject: [PATCH 04/65] Install wasmer 7.2.0-alpha.3 using curl and shell script. Disable (temporarily other workflows). --- .github/workflows/test-and-build-quickjs.yml | 25 ++++++++++++++++---- .github/workflows/test-and-build.yml | 13 ++++++++-- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test-and-build-quickjs.yml b/.github/workflows/test-and-build-quickjs.yml index 5ee4ed9ad..8f75665bc 100644 --- a/.github/workflows/test-and-build-quickjs.yml +++ b/.github/workflows/test-and-build-quickjs.yml @@ -10,6 +10,7 @@ on: jobs: metadata: name: metadata + if: ${{ false }} runs-on: ubuntu-latest outputs: edge_version: ${{ steps.version.outputs.edge_version }} @@ -41,6 +42,7 @@ jobs: quickjs-linux: name: quickjs-linux + if: ${{ false }} runs-on: ubuntu-latest env: SCCACHE_GHA_ENABLED: "true" @@ -106,6 +108,7 @@ jobs: quickjs-macos: name: quickjs-macos + if: ${{ false }} runs-on: macos-latest env: SCCACHE_GHA_ENABLED: "true" @@ -140,12 +143,14 @@ jobs: make build-edge-quickjs-cli CMAKE_BUILD_TYPE=Release JOBS=4 - name: Generate dist + if: ${{ false }} shell: bash run: | set -euo pipefail make dist-only BUILD_DIR=build-edge-quickjs-cli JOBS=4 ZIP_NAME=edge-quickjs-darwin-arm64.zip - name: Upload artifact edge + if: ${{ false }} uses: actions/upload-artifact@v4 with: name: edge-quickjs-darwin-arm64 @@ -184,9 +189,12 @@ jobs: make build-quickjs-wasix - name: Install Wasmer CLI - uses: wasmerio/setup-wasmer@v3 - with: - version: 7.2.0-alpha.3 + shell: bash + run: | + set -euo pipefail + curl https://get.wasmer.io -sSfL | sh -s "v7.2.0-alpha.3" + echo "${HOME}/.wasmer/bin" >> "${GITHUB_PATH}" + "${HOME}/.wasmer/bin/wasmer" --version - name: Test edge (QuickJS WASIX) shell: bash @@ -195,6 +203,7 @@ jobs: make test-wasix-quickjs-only - name: Generate dist + if: ${{ false }} shell: bash run: | set -euo pipefail @@ -205,6 +214,7 @@ jobs: (cd dist && zip -r ../edge-quickjs-wasix.zip bin bin-compat README.md wasmer.toml ssl-certs) - name: Upload artifact edge + if: ${{ false }} uses: actions/upload-artifact@v4 with: name: edge-quickjs-wasix @@ -213,13 +223,13 @@ jobs: publish-nightly: name: publish-nightly + if: ${{ false }} runs-on: ubuntu-latest needs: - metadata - quickjs-linux - quickjs-macos - quickjs-wasix - if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} env: GH_TOKEN: ${{ secrets.RELEASE_PLEASE_GH_TOKEN }} TARGET_REPO: wasmerio/edgejs-nightlies @@ -361,7 +371,12 @@ jobs: "${files[@]}" - name: Install Wasmer CLI - uses: wasmerio/setup-wasmer@v3 + shell: bash + run: | + set -euo pipefail + curl https://get.wasmer.io -sSfL | sh -s "v7.2.0-alpha.3" + echo "${HOME}/.wasmer/bin" >> "${GITHUB_PATH}" + "${HOME}/.wasmer/bin/wasmer" --version - name: Publish to wasmer.wtf env: diff --git a/.github/workflows/test-and-build.yml b/.github/workflows/test-and-build.yml index 8fc342fb5..b4baa0498 100644 --- a/.github/workflows/test-and-build.yml +++ b/.github/workflows/test-and-build.yml @@ -10,6 +10,7 @@ on: jobs: metadata: name: metadata + if: ${{ false }} runs-on: ubuntu-latest outputs: edge_version: ${{ steps.version.outputs.edge_version }} @@ -41,6 +42,7 @@ jobs: v8-linux: name: v8-linux + if: ${{ false }} runs-on: ubuntu-latest env: NAPI_V8_BUILD_METHOD: prebuilt @@ -107,6 +109,7 @@ jobs: v8-macos: name: v8-macos + if: ${{ false }} runs-on: macos-latest env: NAPI_V8_BUILD_METHOD: prebuilt @@ -166,6 +169,7 @@ jobs: v8-wasix: name: v8-wasix + if: ${{ false }} runs-on: ubuntu-latest env: SCCACHE_GHA_ENABLED: "true" @@ -219,13 +223,13 @@ jobs: publish-nightly: name: publish-nightly + if: ${{ false }} runs-on: ubuntu-latest needs: - metadata - v8-linux - v8-macos - v8-wasix - if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} env: GH_TOKEN: ${{ secrets.RELEASE_PLEASE_GH_TOKEN }} TARGET_REPO: wasmerio/edgejs-nightlies @@ -367,7 +371,12 @@ jobs: "${files[@]}" - name: Install Wasmer CLI - uses: wasmerio/setup-wasmer@v3 + shell: bash + run: | + set -euo pipefail + curl https://get.wasmer.io -sSfL | sh -s "v7.2.0-alpha.3" + echo "${HOME}/.wasmer/bin" >> "${GITHUB_PATH}" + "${HOME}/.wasmer/bin/wasmer" --version - name: Publish to wasmer.wtf env: From fbbb21f7500bfa0f80cd84a49e63e579c929bebe Mon Sep 17 00:00:00 2001 From: Sadhbh Code Date: Wed, 3 Jun 2026 00:26:39 +0100 Subject: [PATCH 05/65] Updated sysroot=v2026-05-26.1 wasixcc=v0.4.3 --- .github/workflows/test-and-build-quickjs.yml | 6 +++--- .github/workflows/test-and-build.yml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test-and-build-quickjs.yml b/.github/workflows/test-and-build-quickjs.yml index 8f75665bc..3439ee1fb 100644 --- a/.github/workflows/test-and-build-quickjs.yml +++ b/.github/workflows/test-and-build-quickjs.yml @@ -176,11 +176,11 @@ jobs: submodules: recursive - name: Install wasixcc - uses: wasix-org/wasixcc@v0.4.2 + uses: wasix-org/wasixcc@v0.4.3 with: github_token: ${{ secrets.GITHUB_TOKEN }} - sysroot-tag: v2026-02-16.1 - version: v0.4.2 + sysroot-tag: v2026-05-26.1 + version: v0.4.3 - name: Build edge (QuickJS WASIX) shell: bash diff --git a/.github/workflows/test-and-build.yml b/.github/workflows/test-and-build.yml index b4baa0498..74ad7fd39 100644 --- a/.github/workflows/test-and-build.yml +++ b/.github/workflows/test-and-build.yml @@ -186,11 +186,11 @@ jobs: uses: dtolnay/rust-toolchain@stable - name: Install wasixcc - uses: wasix-org/wasixcc@v0.4.2 + uses: wasix-org/wasixcc@v0.4.3 with: github_token: ${{ secrets.GITHUB_TOKEN }} - sysroot-tag: v2026-02-16.1 - version: v0.4.2 + sysroot-tag: v2026-05-26.1 + version: v0.4.3 - name: Install wasm-tools shell: bash From 566e59a5b5dc451c4970dcc095ed80d6ed9e3120 Mon Sep 17 00:00:00 2001 From: Sadhbh Code Date: Wed, 3 Jun 2026 22:50:28 +0100 Subject: [PATCH 06/65] Force llvm runtime --- scripts/edge-wasix-node-runner.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/edge-wasix-node-runner.sh b/scripts/edge-wasix-node-runner.sh index 151340c45..550257332 100755 --- a/scripts/edge-wasix-node-runner.sh +++ b/scripts/edge-wasix-node-runner.sh @@ -25,6 +25,7 @@ for arg in "$@"; do done exec "${wasmer_bin}" run \ + --llvm \ --net \ --env HOME=/tmp \ --env "NODE_TEST_DIR=${guest_root}/test" \ From 38e01f79c15636badbab17faf6c31eef7d16abc6 Mon Sep 17 00:00:00 2001 From: Sadhbh Code Date: Thu, 4 Jun 2026 01:41:35 +0100 Subject: [PATCH 07/65] Napi/Qjs: SharedArrayBuffer. Ssl certs in wasix tests. --- napi | 2 +- scripts/edge-wasix-node-runner.sh | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/napi b/napi index d8018ee4e..21f22e3cc 160000 --- a/napi +++ b/napi @@ -1 +1 @@ -Subproject commit d8018ee4e859a5ccbcc72df2f3d490e97e35bd87 +Subproject commit 21f22e3cc3b90a2c453d2e4115e0db42c5d8f68a diff --git a/scripts/edge-wasix-node-runner.sh b/scripts/edge-wasix-node-runner.sh index 550257332..0e5007e83 100755 --- a/scripts/edge-wasix-node-runner.sh +++ b/scripts/edge-wasix-node-runner.sh @@ -30,6 +30,7 @@ exec "${wasmer_bin}" run \ --env HOME=/tmp \ --env "NODE_TEST_DIR=${guest_root}/test" \ --volume "${edgejs_root}:${guest_root}" \ + --volume "${edgejs_root}/ssl-certs:/usr/local/ssl" \ --cwd "${guest_root}" \ "${package_dir}" \ -- "${guest_args[@]}" From c7072acfce0e3202889f64ea45e10890a53c3edf Mon Sep 17 00:00:00 2001 From: Sadhbh Code Date: Thu, 4 Jun 2026 02:29:02 +0100 Subject: [PATCH 08/65] Support spawn fork --- .gitmodules | 3 +++ deps/libuv-wasix | 1 + wasix/setup-wasix-deps.sh | 2 -- 3 files changed, 4 insertions(+), 2 deletions(-) create mode 160000 deps/libuv-wasix diff --git a/.gitmodules b/.gitmodules index 8b27b9074..063fee22b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -15,3 +15,6 @@ [submodule "ssl-certs"] path = ssl-certs url = https://github.com/wasix-org/ssl-certs.git +[submodule "deps/libuv-wasix"] + path = deps/libuv-wasix + url = git@github.com:wasix-org/libuv.git diff --git a/deps/libuv-wasix b/deps/libuv-wasix new file mode 160000 index 000000000..8aa76d1f5 --- /dev/null +++ b/deps/libuv-wasix @@ -0,0 +1 @@ +Subproject commit 8aa76d1f568bbbb0787fd96ecc67e92dd2f91630 diff --git a/wasix/setup-wasix-deps.sh b/wasix/setup-wasix-deps.sh index d03d1919c..128f1ee06 100755 --- a/wasix/setup-wasix-deps.sh +++ b/wasix/setup-wasix-deps.sh @@ -35,8 +35,6 @@ clone_or_update() { } mkdir -p "${DEPS_DIR}" -# Keep the upstream branch name until the external repo renames it. -clone_or_update "${DEPS_DIR}/libuv-wasix" "https://github.com/wasix-org/libuv.git" "ubi" clone_or_update "${DEPS_DIR}/openssl-wasix" "https://github.com/wasix-org/openssl.git" "master" echo "WASIX deps are ready under ${DEPS_DIR}" From 00a1d5a1cd4d6607c4775c79bd7e9fc07d94c661 Mon Sep 17 00:00:00 2001 From: Sadhbh Code Date: Thu, 4 Jun 2026 18:53:19 +0100 Subject: [PATCH 09/65] Switching libuv to Artemy's branch --- deps/libuv-wasix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps/libuv-wasix b/deps/libuv-wasix index 8aa76d1f5..ba06698ba 160000 --- a/deps/libuv-wasix +++ b/deps/libuv-wasix @@ -1 +1 @@ -Subproject commit 8aa76d1f568bbbb0787fd96ecc67e92dd2f91630 +Subproject commit ba06698ba3f3b508a7a4eeacb23e0c911ebf4576 From 9fa61f1888f34c0785f54da8dd99ae193e536439 Mon Sep 17 00:00:00 2001 From: Sadhbh Code Date: Thu, 4 Jun 2026 22:02:32 +0100 Subject: [PATCH 10/65] UV keep alive fix (disable unsupported options) --- deps/libuv-wasix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps/libuv-wasix b/deps/libuv-wasix index ba06698ba..8d5374405 160000 --- a/deps/libuv-wasix +++ b/deps/libuv-wasix @@ -1 +1 @@ -Subproject commit ba06698ba3f3b508a7a4eeacb23e0c911ebf4576 +Subproject commit 8d537440533cfc290e33c7bcbf181ab414dd1850 From 32a4e43a52930be351151d7c9cfbce13faaf545a Mon Sep 17 00:00:00 2001 From: Sadhbh Code Date: Thu, 4 Jun 2026 22:57:16 +0100 Subject: [PATCH 11/65] Updated libuv branch --- deps/libuv-wasix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps/libuv-wasix b/deps/libuv-wasix index 8d5374405..5539194c8 160000 --- a/deps/libuv-wasix +++ b/deps/libuv-wasix @@ -1 +1 @@ -Subproject commit 8d537440533cfc290e33c7bcbf181ab414dd1850 +Subproject commit 5539194c8f9e751e4a7da94cd4fb69722736e9ae From 64faa6caba3648b7393c69c9a339a4b90f2a697e Mon Sep 17 00:00:00 2001 From: Sadhbh Code Date: Fri, 5 Jun 2026 13:53:18 +0100 Subject: [PATCH 12/65] Respect address family (IPv4 or IPv6) when asked GetAddrInfo --- src/edge_cares_wrap.cc | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/src/edge_cares_wrap.cc b/src/edge_cares_wrap.cc index 4c1f81487..41a32f07c 100644 --- a/src/edge_cares_wrap.cc +++ b/src/edge_cares_wrap.cc @@ -79,6 +79,7 @@ struct CaresReqWrap { bool finalized = false; bool orphaned = false; uint8_t order = kDnsOrderVerbatim; + int32_t family = 0; bool ttl = false; std::string hostname; std::string binding_name; @@ -1807,18 +1808,24 @@ void OnGetAddrInfo(uv_getaddrinfo_t* req, int status, addrinfo* res) { } }; - switch (wrap->order) { - case kDnsOrderIpv4First: - add(true, false); - add(false, true); - break; - case kDnsOrderIpv6First: - add(false, true); - add(true, false); - break; - default: - add(true, true); - break; + if (wrap->family == 4) { + add(true, false); + } else if (wrap->family == 6) { + add(false, true); + } else { + switch (wrap->order) { + case kDnsOrderIpv4First: + add(true, false); + add(false, true); + break; + case kDnsOrderIpv6First: + add(false, true); + add(true, false); + break; + default: + add(true, true); + break; + } } if (n == 0) { @@ -1902,6 +1909,7 @@ napi_value CaresGetAddrInfo(napi_env env, napi_callback_info info) { req->env = env; req->in_flight = true; req->orphaned = false; + req->family = family; req->hostname = ToAsciiHostname(ValueToUtf8(env, argv[1])); PinReqObject(req); From bb7d346b60d3c24aa401277f9070ecff7259185e Mon Sep 17 00:00:00 2001 From: Sadhbh Code Date: Fri, 5 Jun 2026 15:27:24 +0100 Subject: [PATCH 13/65] Set outbound ack limit for wasi --- src/internal_binding/binding_http2.cc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/internal_binding/binding_http2.cc b/src/internal_binding/binding_http2.cc index 69413587c..c877dfc16 100644 --- a/src/internal_binding/binding_http2.cc +++ b/src/internal_binding/binding_http2.cc @@ -3937,6 +3937,9 @@ napi_value SessionCtor(napi_env env, napi_callback_info info) { if (option != nullptr) { nghttp2_option_set_no_closed_streams(option, 1); nghttp2_option_set_no_auto_window_update(option, 1); +#if defined(__wasi__) || defined(__wasm32__) + nghttp2_option_set_max_outbound_ack(option, 512); +#endif if (wrap->type == kSessionTypeClient) { nghttp2_option_set_builtin_recv_extension_type(option, NGHTTP2_ALTSVC); nghttp2_option_set_builtin_recv_extension_type(option, NGHTTP2_ORIGIN); From 9aefbbe29753d32a43dcd3b11b9cd8dbbcb99b30 Mon Sep 17 00:00:00 2001 From: Sadhbh Code Date: Fri, 5 Jun 2026 16:44:23 +0100 Subject: [PATCH 14/65] Isolate mapped host directories for wasix node test runner --- scripts/edge-wasix-node-runner.sh | 104 +++++++++++++++++++++++++++--- 1 file changed, 95 insertions(+), 9 deletions(-) diff --git a/scripts/edge-wasix-node-runner.sh b/scripts/edge-wasix-node-runner.sh index 0e5007e83..53670fcaf 100755 --- a/scripts/edge-wasix-node-runner.sh +++ b/scripts/edge-wasix-node-runner.sh @@ -8,29 +8,115 @@ edgejs_root="${EDGEJS_ROOT:-${default_edgejs_root}}" wasmer_bin="${WASMER_BIN:-wasmer}" package_dir="${WASIX_EDGEJS_PACKAGE_DIR:-${edgejs_root}/quickjs-wasm}" guest_root="${WASIX_EDGEJS_GUEST_ROOT:-/workspace}" +guest_test_tmp_root="${WASIX_EDGEJS_GUEST_TEST_TMP_ROOT:-/tmp/edgejs-node-test}" +workspace_dirs_csv="${WASIX_EDGEJS_WORKSPACE_DIRS:-test,lib,deps,node,assets}" +wasmer_stack_args=() +created_run_root=0 -guest_args=() -for arg in "$@"; do +if [[ -n "${WASMER_STACK_SIZE:-}" ]]; then + wasmer_stack_args+=(--stack-size "${WASMER_STACK_SIZE}") +fi + +if [[ -n "${WASIX_EDGEJS_HOST_RUN_ROOT:-}" ]]; then + host_run_root="${WASIX_EDGEJS_HOST_RUN_ROOT}" +else + host_run_root="$(mktemp -d "${TMPDIR:-/tmp}/edgejs-wasix-node.XXXXXX")" + created_run_root=1 +fi + +host_workspace_root="${host_run_root}/workspace" +host_test_tmp_root="${host_run_root}/node-test" +mkdir -p "${host_workspace_root}" "${host_test_tmp_root}" + +cleanup() { + local status=$? + if [[ "${WASIX_EDGEJS_KEEP_TMP:-0}" == "1" ]]; then + printf 'Preserving WASIX EdgeJS test run root: %s\n' "${host_run_root}" >&2 + elif [[ "${WASIX_EDGEJS_KEEP_TMP:-0}" == "failed" && "${status}" != "0" ]]; then + printf 'Preserving failed WASIX EdgeJS test run root: %s\n' "${host_run_root}" >&2 + elif [[ "${created_run_root}" == "1" ]]; then + rm -rf "${host_run_root}" + fi + return "${status}" +} +trap cleanup EXIT + +test_serial_input="${EDGEJS_WASIX_TEST_ID:-$*}" +test_serial_hash="$(printf '%s' "${test_serial_input}" | cksum | awk '{print $1}')" +test_serial_id="${TEST_SERIAL_ID:-wasix-${test_serial_hash}-$$}" + +rewrite_guest_path_arg() { + local arg="$1" case "${arg}" in "${edgejs_root}"/*) - guest_args+=("${guest_root}/${arg#"${edgejs_root}/"}") + printf '%s\n' "${guest_root}/${arg#"${edgejs_root}/"}" ;; "${edgejs_root}") - guest_args+=("${guest_root}") + printf '%s\n' "${guest_root}" + ;; + *=${edgejs_root}/*) + local key="${arg%%=*}" + local value="${arg#*=}" + printf '%s\n' "${key}=${guest_root}/${value#"${edgejs_root}/"}" + ;; + *=${edgejs_root}) + local key="${arg%%=*}" + printf '%s\n' "${key}=${guest_root}" ;; *) - guest_args+=("${arg}") + printf '%s\n' "${arg}" ;; esac +} + +guest_args=() +for arg in "$@"; do + guest_args+=("$(rewrite_guest_path_arg "${arg}")") done -exec "${wasmer_bin}" run \ +volume_args=( + --volume "${host_workspace_root}:${guest_root}" + --volume "${host_test_tmp_root}:${guest_test_tmp_root}" + --volume "${edgejs_root}/ssl-certs:/usr/local/ssl" +) + +IFS=',' read -r -a workspace_dirs <<< "${workspace_dirs_csv}" +for workspace_dir in "${workspace_dirs[@]}"; do + workspace_dir="${workspace_dir#"${workspace_dir%%[![:space:]]*}"}" + workspace_dir="${workspace_dir%"${workspace_dir##*[![:space:]]}"}" + [[ -z "${workspace_dir}" ]] && continue + if [[ ! -d "${edgejs_root}/${workspace_dir}" ]]; then + printf 'warning: skipping missing WASIX EdgeJS workspace dir: %s\n' "${edgejs_root}/${workspace_dir}" >&2 + continue + fi + volume_args+=(--volume "${edgejs_root}/${workspace_dir}:${guest_root}/${workspace_dir}") +done + +if [[ "${WASIX_EDGEJS_TRACE:-0}" == "1" ]]; then + { + printf '%q ' "${wasmer_bin}" run \ + --llvm \ + "${wasmer_stack_args[@]+"${wasmer_stack_args[@]}"}" \ + --net \ + --env HOME=/tmp \ + --env "NODE_TEST_DIR=${guest_test_tmp_root}" \ + --env "TEST_SERIAL_ID=${test_serial_id}" \ + "${volume_args[@]}" \ + --cwd "${guest_root}" \ + "${package_dir}" \ + -- "${guest_args[@]}" + printf '\n' + } >&2 +fi + +"${wasmer_bin}" run \ --llvm \ + "${wasmer_stack_args[@]+"${wasmer_stack_args[@]}"}" \ --net \ --env HOME=/tmp \ - --env "NODE_TEST_DIR=${guest_root}/test" \ - --volume "${edgejs_root}:${guest_root}" \ - --volume "${edgejs_root}/ssl-certs:/usr/local/ssl" \ + --env "NODE_TEST_DIR=${guest_test_tmp_root}" \ + --env "TEST_SERIAL_ID=${test_serial_id}" \ + "${volume_args[@]}" \ --cwd "${guest_root}" \ "${package_dir}" \ -- "${guest_args[@]}" From 929a9151c336b5862839d7b3d3e2bec219f852ca Mon Sep 17 00:00:00 2001 From: Sadhbh Code Date: Fri, 5 Jun 2026 17:44:41 +0100 Subject: [PATCH 15/65] c-ares requests are now tracked per channel, cancellation completes still-active requests with ECANCELLED, reverse lookup includes h_name, and loopback reverse/nameinfo returns localhost --- src/edge_cares_wrap.cc | 77 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 72 insertions(+), 5 deletions(-) diff --git a/src/edge_cares_wrap.cc b/src/edge_cares_wrap.cc index 41a32f07c..99fa24b52 100644 --- a/src/edge_cares_wrap.cc +++ b/src/edge_cares_wrap.cc @@ -110,6 +110,7 @@ struct ChannelWrap { bool destroy_scheduled = false; uv_timer_t* timer_handle = nullptr; std::unordered_map tasks; + std::unordered_set active_reqs; }; struct QueryMethodData { @@ -164,6 +165,13 @@ napi_value GetRefValue(napi_env env, napi_ref ref); napi_value GetCaresReqOwner(napi_env env, void* data); void CancelCaresReq(void* data); void EnsureReqAsyncWrap(CaresReqWrap* req, napi_value req_obj); +void CompleteQuery(CaresReqWrap* req, + int status, + const unsigned char* buf, + int len, + hostent* host, + bool from_host); +void CancelChannelRequests(ChannelWrap* channel); bool EnvCleanupInProgress(napi_env env) { CaresEnvState* state = GetCaresState(env); @@ -257,7 +265,11 @@ void CancelCaresReq(void* data) { return; } if (req->used_query && req->channel != nullptr && req->channel->channel != nullptr) { - ares_cancel(req->channel->channel); + ChannelWrap* channel = req->channel; + ares_cancel(channel->channel); + if (channel->active_reqs.find(req) != channel->active_reqs.end()) { + CompleteQuery(req, ARES_ECANCELLED, nullptr, 0, nullptr, false); + } } } @@ -1417,11 +1429,21 @@ int ParseSoaOnlyReply(napi_env env, const unsigned char* buf, int len, napi_valu int ParseReverseHost(napi_env env, hostent* host, napi_value* out) { napi_value ret = nullptr; napi_create_array(env, &ret); + std::unordered_set seen; + if (host != nullptr && host->h_name != nullptr && host->h_name[0] != '\0') { + if (!AppendArrayValue(env, ret, MakeStringUtf8(env, host->h_name))) { + return ARES_ENOMEM; + } + seen.insert(host->h_name); + } if (host != nullptr && host->h_aliases != nullptr) { for (uint32_t i = 0; host->h_aliases[i] != nullptr; ++i) { + if (host->h_aliases[i] == nullptr || host->h_aliases[i][0] == '\0') continue; + if (seen.find(host->h_aliases[i]) != seen.end()) continue; if (!AppendArrayValue(env, ret, MakeStringUtf8(env, host->h_aliases[i]))) { return ARES_ENOMEM; } + seen.insert(host->h_aliases[i]); } } *out = ret; @@ -1439,6 +1461,7 @@ void CompleteQuery(CaresReqWrap* req, ChannelWrap* channel = req->channel; if (channel != nullptr) { channel->query_last_ok = (status != ARES_ECONNREFUSED); + channel->active_reqs.erase(req); } napi_env env = req->env; @@ -1605,6 +1628,20 @@ int DispatchQuery(ChannelWrap* channel, CaresReqWrap* req, const QueryMethodData return UV_EINVAL; } + if (req->hostname == "127.0.0.1" || req->hostname == "::1") { + channel->active_reqs.insert(req); + char localhost[] = "localhost"; + char* aliases[] = {localhost, nullptr}; + hostent host{}; + host.h_name = localhost; + host.h_aliases = aliases; + host.h_addrtype = family; + host.h_length = length; + CompleteQuery(req, ARES_SUCCESS, nullptr, 0, &host, true); + return 0; + } + + channel->active_reqs.insert(req); ares_gethostbyaddr(channel->channel, address_buffer, length, @@ -1615,6 +1652,7 @@ int DispatchQuery(ChannelWrap* channel, CaresReqWrap* req, const QueryMethodData return 0; } + channel->active_reqs.insert(req); ares_query_dnsrec(channel->channel, req->hostname.c_str(), ARES_CLASS_IN, @@ -1651,7 +1689,7 @@ void OnCaresEnvCleanup(void* arg) { for (ChannelWrap* channel : channels) { if (channel == nullptr || channel->channel == nullptr) continue; - ares_cancel(channel->channel); + CancelChannelRequests(channel); } for (ChannelWrap* channel : pinned_channels) { @@ -1961,6 +1999,20 @@ napi_value CaresGetNameInfo(napi_env env, napi_callback_info info) { int32_t port = 0; napi_get_value_int32(env, argv[2], &port); + if (host == "127.0.0.1" || host == "::1") { + const std::string service = std::to_string(port); + napi_value cb_argv[3] = { + MakeInt32(env, 0), + MakeStringUtf8(env, "localhost"), + MakeStringUtf8(env, service.c_str()), + }; + InvokeOnComplete(env, req, 3, cb_argv); + UntrackPendingReq(req); + MarkReqComplete(req); + CleanupReqAfterAsync(req); + return MakeInt32(env, 0); + } + sockaddr_storage storage{}; int rc = uv_ip4_addr(host.c_str(), port, reinterpret_cast(&storage)); if (rc != 0) { @@ -2051,6 +2103,23 @@ napi_value CaresStrError(napi_env env, napi_callback_info info) { return MakeStringUtf8(env, ares_strerror(code)); } +void CancelChannelRequests(ChannelWrap* channel) { + if (channel == nullptr || channel->channel == nullptr) return; + + std::vector reqs; + reqs.reserve(channel->active_reqs.size()); + for (CaresReqWrap* req : channel->active_reqs) { + reqs.push_back(req); + } + + ares_cancel(channel->channel); + + for (CaresReqWrap* req : reqs) { + if (channel->active_reqs.find(req) == channel->active_reqs.end()) continue; + CompleteQuery(req, ARES_ECANCELLED, nullptr, 0, nullptr, false); + } +} + napi_value ChannelCancel(napi_env env, napi_callback_info info) { napi_value self = nullptr; size_t argc = 0; @@ -2058,9 +2127,7 @@ napi_value ChannelCancel(napi_env env, napi_callback_info info) { ChannelWrap* channel = nullptr; napi_unwrap(env, self, reinterpret_cast(&channel)); - if (channel != nullptr && channel->channel != nullptr) { - ares_cancel(channel->channel); - } + CancelChannelRequests(channel); return MakeUndefined(env); } From 61ba9604327a7326a555c2d537d7214421c60a9c Mon Sep 17 00:00:00 2001 From: Sadhbh Code Date: Sat, 6 Jun 2026 02:19:23 +0100 Subject: [PATCH 16/65] various fixes --- lib/buffer.js | 25 ++++++++++++++++++++++++- lib/child_process.js | 18 ++++++++++++++++++ lib/net.js | 8 +++++++- scripts/edge-wasix-node-runner.sh | 5 ++++- src/edge_fs.cc | 12 +++++++++++- src/edge_process.cc | 4 ++++ src/internal_binding/binding_fs.cc | 12 +++++++++++- wasix/src/wasix_compat.cc | 2 +- 8 files changed, 80 insertions(+), 6 deletions(-) diff --git a/lib/buffer.js b/lib/buffer.js index c1b61fd17..76f075d92 100644 --- a/lib/buffer.js +++ b/lib/buffer.js @@ -26,6 +26,7 @@ const { ArrayBufferIsView, ArrayIsArray, ArrayPrototypeForEach, + Error, MathFloor, MathMin, MathTrunc, @@ -39,6 +40,8 @@ const { ObjectSetPrototypeOf, RegExpPrototypeSymbolReplace, StringPrototypeCharCodeAt, + StringPrototypeIncludes, + StringPrototypeSplit, StringPrototypeSlice, StringPrototypeToLowerCase, StringPrototypeTrim, @@ -190,11 +193,31 @@ const bufferWarning = 'Buffer() is deprecated due to security and usability ' + 'issues. Please use the Buffer.alloc(), ' + 'Buffer.allocUnsafe(), or Buffer.from() methods instead.'; +function isNodeModulesStackLine(line) { + return StringPrototypeIncludes(line, '/node_modules/') || + StringPrototypeIncludes(line, '\\node_modules\\') || + StringPrototypeIncludes(line, '/node_modules\\') || + StringPrototypeIncludes(line, '\\node_modules/'); +} + +function isInsideNodeModulesFromStack() { + const stack = new Error().stack; + if (typeof stack !== 'string') return isInsideNodeModules(3); + const lines = StringPrototypeSplit(stack, '\n'); + for (let i = 1; i < lines.length; ++i) { + const line = StringPrototypeTrim(lines[i]); + if (line === '') continue; + if (StringPrototypeIncludes(line, 'node:')) continue; + return isNodeModulesStackLine(line); + } + return isInsideNodeModules(3); +} + function showFlaggedDeprecation() { if (bufferWarningAlreadyEmitted || ++nodeModulesCheckCounter > 10000 || (!require('internal/options').getOptionValue('--pending-deprecation') && - isInsideNodeModules(3))) { + isInsideNodeModulesFromStack())) { // We don't emit a warning, because we either: // - Already did so, or // - Already checked too many times whether a call is coming diff --git a/lib/child_process.js b/lib/child_process.js index 17c6b69c1..159760f06 100644 --- a/lib/child_process.js +++ b/lib/child_process.js @@ -564,6 +564,22 @@ function copyPermissionModelFlagsToEnv(env, key, args) { } let emittedDEP0190Already = false; + +function rewriteWasixMultilineEvalArg(file, args) { + if (file !== process.execPath || process.platform !== 'wasi') return; + if (args.length < 2 || typeof args[1] !== 'string') return; + if (args[0] !== '-e' && args[0] !== '--eval' && + args[0] !== '-p' && args[0] !== '--print') { + return; + } + if (!StringPrototypeIncludes(args[1], '\n') && + !StringPrototypeIncludes(args[1], '\r')) { + return; + } + const encoded = Buffer.from(args[1], 'utf8').toString('base64'); + args[1] = `eval(Buffer.from('${encoded}', 'base64').toString())`; +} + function normalizeSpawnArguments(file, args, options) { validateString(file, 'file'); validateArgumentNullCheck(file, 'file'); @@ -674,6 +690,8 @@ function normalizeSpawnArguments(file, args, options) { } } + rewriteWasixMultilineEvalArg(file, args); + if (typeof options.argv0 === 'string') { ArrayPrototypeUnshift(args, options.argv0); } else { diff --git a/lib/net.js b/lib/net.js index a391e9da3..ca1621d10 100644 --- a/lib/net.js +++ b/lib/net.js @@ -76,6 +76,7 @@ const { PipeConnectWrap, constants: PipeConstants, } = internalBinding('pipe_wrap'); +const { TTY } = internalBinding('tty_wrap'); const { newAsyncId, defaultTriggerAsyncIdScope, @@ -191,6 +192,11 @@ function createHandle(fd, is_server) { ); } + if (type === 'TTY' && !is_server) { + const ctx = {}; + return new TTY(fd, ctx); + } + throw new ERR_INVALID_FD_TYPE(type); } @@ -427,7 +433,7 @@ function Socket(options) { // a valid `PIPE` or `TCP` descriptor this._handle = createHandle(fd, false); - err = this._handle.open(fd); + err = typeof this._handle.open === 'function' ? this._handle.open(fd) : 0; // While difficult to fabricate, in some architectures // `open` may return an error code for valid file descriptors diff --git a/scripts/edge-wasix-node-runner.sh b/scripts/edge-wasix-node-runner.sh index 53670fcaf..26908375b 100755 --- a/scripts/edge-wasix-node-runner.sh +++ b/scripts/edge-wasix-node-runner.sh @@ -9,7 +9,8 @@ wasmer_bin="${WASMER_BIN:-wasmer}" package_dir="${WASIX_EDGEJS_PACKAGE_DIR:-${edgejs_root}/quickjs-wasm}" guest_root="${WASIX_EDGEJS_GUEST_ROOT:-/workspace}" guest_test_tmp_root="${WASIX_EDGEJS_GUEST_TEST_TMP_ROOT:-/tmp/edgejs-node-test}" -workspace_dirs_csv="${WASIX_EDGEJS_WORKSPACE_DIRS:-test,lib,deps,node,assets}" +workspace_dirs_csv="${WASIX_EDGEJS_WORKSPACE_DIRS:-test,lib,deps,node,assets,build-quickjs-wasix}" +guest_exec_path="${WASIX_EDGEJS_GUEST_EXEC_PATH:-${guest_root}/build-quickjs-wasix/edgejs.wasm}" wasmer_stack_args=() created_run_root=0 @@ -99,6 +100,7 @@ if [[ "${WASIX_EDGEJS_TRACE:-0}" == "1" ]]; then "${wasmer_stack_args[@]+"${wasmer_stack_args[@]}"}" \ --net \ --env HOME=/tmp \ + --env "EDGE_EXEC_PATH=${guest_exec_path}" \ --env "NODE_TEST_DIR=${guest_test_tmp_root}" \ --env "TEST_SERIAL_ID=${test_serial_id}" \ "${volume_args[@]}" \ @@ -114,6 +116,7 @@ fi "${wasmer_stack_args[@]+"${wasmer_stack_args[@]}"}" \ --net \ --env HOME=/tmp \ + --env "EDGE_EXEC_PATH=${guest_exec_path}" \ --env "NODE_TEST_DIR=${guest_test_tmp_root}" \ --env "TEST_SERIAL_ID=${test_serial_id}" \ "${volume_args[@]}" \ diff --git a/src/edge_fs.cc b/src/edge_fs.cc index 02a1165ff..43d24198c 100644 --- a/src/edge_fs.cc +++ b/src/edge_fs.cc @@ -813,9 +813,19 @@ napi_value BindingRealpath(napi_env env, napi_callback_info info) { // birthtime_sec, birthtime_nsec (18 elements). constexpr size_t kStatArrayLength = 18; +uint64_t NormalizeStatMode(uint64_t mode) { +#if defined(__wasi__) + if ((mode & 0777) == 0) { + if (S_ISDIR(mode)) return mode | 0700; + if (S_ISREG(mode)) return mode | 0600; + } +#endif + return mode; +} + void StatToArray(const uv_stat_t* s, double* out) { out[0] = static_cast(s->st_dev); - out[1] = static_cast(s->st_mode); + out[1] = static_cast(NormalizeStatMode(static_cast(s->st_mode))); out[2] = static_cast(s->st_nlink); out[3] = static_cast(s->st_uid); out[4] = static_cast(s->st_gid); diff --git a/src/edge_process.cc b/src/edge_process.cc index 3c67258ad..ddd849424 100644 --- a/src/edge_process.cc +++ b/src/edge_process.cc @@ -463,6 +463,8 @@ const char* DetectPlatform() { return "darwin"; #elif defined(__linux__) return "linux"; +#elif defined(__wasi__) + return "wasi"; #elif defined(__sun) return "sunos"; #elif defined(_AIX) @@ -515,6 +517,8 @@ std::string DetectExecPath() { return "edge"; #elif defined(_WIN32) return "edge.exe"; +#elif defined(__wasi__) + return "/bin/edge"; #else return "edge"; #endif diff --git a/src/internal_binding/binding_fs.cc b/src/internal_binding/binding_fs.cc index e220f8f41..c18dd40d5 100644 --- a/src/internal_binding/binding_fs.cc +++ b/src/internal_binding/binding_fs.cc @@ -378,10 +378,20 @@ napi_value CreateTypedStatsArray(napi_env env, size_t length, bool as_bigint, na return out; } +uint64_t NormalizeUvMode(uint64_t mode) { +#if defined(__wasi__) + if ((mode & 0777) == 0) { + if (S_ISDIR(mode)) return mode | 0700; + if (S_ISREG(mode)) return mode | 0600; + } +#endif + return mode; +} + void PopulateStatsArrayFromUv(const uv_stat_t* stat, double* out) { if (stat == nullptr || out == nullptr) return; out[0] = static_cast(stat->st_dev); - out[1] = static_cast(stat->st_mode); + out[1] = static_cast(NormalizeUvMode(static_cast(stat->st_mode))); out[2] = static_cast(stat->st_nlink); out[3] = static_cast(stat->st_uid); out[4] = static_cast(stat->st_gid); diff --git a/wasix/src/wasix_compat.cc b/wasix/src/wasix_compat.cc index e118f5efd..27f471ee3 100644 --- a/wasix/src/wasix_compat.cc +++ b/wasix/src/wasix_compat.cc @@ -34,7 +34,7 @@ extern "C" int uv_resident_set_memory(size_t* rss) { if (rss != nullptr) { *rss = 0; } - return -ENOSYS; + return 0; } extern "C" int uv_cpu_info(uv_cpu_info_t** cpu_infos, int* count) { From 27118329f47b9876c3f415bf42669f9cc4a6568b Mon Sep 17 00:00:00 2001 From: Sadhbh Code Date: Sat, 6 Jun 2026 15:05:12 +0100 Subject: [PATCH 17/65] Fixes around edge process wrap, plus w/a for edge -e eval --- lib/buffer.js | 25 +--------------- lib/child_process.js | 17 ----------- lib/net.js | 6 ---- src/edge_process_wrap.cc | 64 ++++++++++++++++++++++++++++++++++++++-- src/edge_spawn_sync.cc | 51 ++++++++++++++++++++++++++++++++ src/edge_util.cc | 13 ++++++-- 6 files changed, 125 insertions(+), 51 deletions(-) diff --git a/lib/buffer.js b/lib/buffer.js index 76f075d92..c1b61fd17 100644 --- a/lib/buffer.js +++ b/lib/buffer.js @@ -26,7 +26,6 @@ const { ArrayBufferIsView, ArrayIsArray, ArrayPrototypeForEach, - Error, MathFloor, MathMin, MathTrunc, @@ -40,8 +39,6 @@ const { ObjectSetPrototypeOf, RegExpPrototypeSymbolReplace, StringPrototypeCharCodeAt, - StringPrototypeIncludes, - StringPrototypeSplit, StringPrototypeSlice, StringPrototypeToLowerCase, StringPrototypeTrim, @@ -193,31 +190,11 @@ const bufferWarning = 'Buffer() is deprecated due to security and usability ' + 'issues. Please use the Buffer.alloc(), ' + 'Buffer.allocUnsafe(), or Buffer.from() methods instead.'; -function isNodeModulesStackLine(line) { - return StringPrototypeIncludes(line, '/node_modules/') || - StringPrototypeIncludes(line, '\\node_modules\\') || - StringPrototypeIncludes(line, '/node_modules\\') || - StringPrototypeIncludes(line, '\\node_modules/'); -} - -function isInsideNodeModulesFromStack() { - const stack = new Error().stack; - if (typeof stack !== 'string') return isInsideNodeModules(3); - const lines = StringPrototypeSplit(stack, '\n'); - for (let i = 1; i < lines.length; ++i) { - const line = StringPrototypeTrim(lines[i]); - if (line === '') continue; - if (StringPrototypeIncludes(line, 'node:')) continue; - return isNodeModulesStackLine(line); - } - return isInsideNodeModules(3); -} - function showFlaggedDeprecation() { if (bufferWarningAlreadyEmitted || ++nodeModulesCheckCounter > 10000 || (!require('internal/options').getOptionValue('--pending-deprecation') && - isInsideNodeModulesFromStack())) { + isInsideNodeModules(3))) { // We don't emit a warning, because we either: // - Already did so, or // - Already checked too many times whether a call is coming diff --git a/lib/child_process.js b/lib/child_process.js index 159760f06..330e3cf11 100644 --- a/lib/child_process.js +++ b/lib/child_process.js @@ -565,21 +565,6 @@ function copyPermissionModelFlagsToEnv(env, key, args) { let emittedDEP0190Already = false; -function rewriteWasixMultilineEvalArg(file, args) { - if (file !== process.execPath || process.platform !== 'wasi') return; - if (args.length < 2 || typeof args[1] !== 'string') return; - if (args[0] !== '-e' && args[0] !== '--eval' && - args[0] !== '-p' && args[0] !== '--print') { - return; - } - if (!StringPrototypeIncludes(args[1], '\n') && - !StringPrototypeIncludes(args[1], '\r')) { - return; - } - const encoded = Buffer.from(args[1], 'utf8').toString('base64'); - args[1] = `eval(Buffer.from('${encoded}', 'base64').toString())`; -} - function normalizeSpawnArguments(file, args, options) { validateString(file, 'file'); validateArgumentNullCheck(file, 'file'); @@ -690,8 +675,6 @@ function normalizeSpawnArguments(file, args, options) { } } - rewriteWasixMultilineEvalArg(file, args); - if (typeof options.argv0 === 'string') { ArrayPrototypeUnshift(args, options.argv0); } else { diff --git a/lib/net.js b/lib/net.js index ca1621d10..d4fdda6dd 100644 --- a/lib/net.js +++ b/lib/net.js @@ -76,7 +76,6 @@ const { PipeConnectWrap, constants: PipeConstants, } = internalBinding('pipe_wrap'); -const { TTY } = internalBinding('tty_wrap'); const { newAsyncId, defaultTriggerAsyncIdScope, @@ -192,11 +191,6 @@ function createHandle(fd, is_server) { ); } - if (type === 'TTY' && !is_server) { - const ctx = {}; - return new TTY(fd, ctx); - } - throw new ERR_INVALID_FD_TYPE(type); } diff --git a/src/edge_process_wrap.cc b/src/edge_process_wrap.cc index 18b96f70b..682f74d5f 100644 --- a/src/edge_process_wrap.cc +++ b/src/edge_process_wrap.cc @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -38,6 +39,65 @@ struct ProcessWrap { std::mutex g_live_child_pids_mutex; std::unordered_set g_live_child_pids; +std::string Base64Encode(std::string_view input) { + static constexpr char kTable[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + std::string out; + out.reserve(((input.size() + 2) / 3) * 4); + size_t i = 0; + for (; i + 3 <= input.size(); i += 3) { + const uint32_t chunk = (static_cast(input[i]) << 16) | + (static_cast(input[i + 1]) << 8) | + static_cast(input[i + 2]); + out.push_back(kTable[(chunk >> 18) & 0x3f]); + out.push_back(kTable[(chunk >> 12) & 0x3f]); + out.push_back(kTable[(chunk >> 6) & 0x3f]); + out.push_back(kTable[chunk & 0x3f]); + } + const size_t remaining = input.size() - i; + if (remaining == 1) { + const uint32_t chunk = static_cast(input[i]) << 16; + out.push_back(kTable[(chunk >> 18) & 0x3f]); + out.push_back(kTable[(chunk >> 12) & 0x3f]); + out.push_back('='); + out.push_back('='); + } else if (remaining == 2) { + const uint32_t chunk = (static_cast(input[i]) << 16) | + (static_cast(input[i + 1]) << 8); + out.push_back(kTable[(chunk >> 18) & 0x3f]); + out.push_back(kTable[(chunk >> 12) & 0x3f]); + out.push_back(kTable[(chunk >> 6) & 0x3f]); + out.push_back('='); + } + return out; +} + +bool IsEvalOrPrintFlag(const std::string& arg) { + return arg == "-e" || arg == "--eval" || arg == "-p" || arg == "--print"; +} + +void RewriteWasixMultilineEvalArgs(std::vector* args) { +#ifdef __wasi__ + if (args == nullptr || args->size() < 3) return; + if (!IsEvalOrPrintFlag((*args)[1])) return; + std::string& script = (*args)[2]; + if (script.find('\n') == std::string::npos && script.find('\r') == std::string::npos) return; + script = "eval(Buffer.from('" + Base64Encode(script) + "', 'base64').toString())"; +#else + (void)args; +#endif +} + +void RefreshArgvPointers(std::vector* storage, std::vector* out) { + if (storage == nullptr || out == nullptr) return; + out->clear(); + out->reserve(storage->size() + 1); + for (std::string& arg : *storage) { + out->push_back(const_cast(arg.c_str())); + } + out->push_back(nullptr); +} + napi_value MakeInt32(napi_env env, int32_t value) { napi_value out = nullptr; napi_create_int32(env, value, &out); @@ -479,9 +539,9 @@ napi_value ProcessSpawn(napi_env env, napi_callback_info info) { } if (args_storage.empty()) { args_storage.push_back(file); - args.push_back(const_cast(args_storage[0].c_str())); - args.push_back(nullptr); } + RewriteWasixMultilineEvalArgs(&args_storage); + RefreshArgvPointers(&args_storage, &args); napi_value env_pairs_value = nullptr; std::vector env_storage; diff --git a/src/edge_spawn_sync.cc b/src/edge_spawn_sync.cc index ee78da6b2..d9d303881 100644 --- a/src/edge_spawn_sync.cc +++ b/src/edge_spawn_sync.cc @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -133,6 +134,55 @@ std::string ValueToUtf8(napi_env env, napi_value value) { return out; } +std::string Base64Encode(std::string_view input) { + static constexpr char kTable[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + std::string out; + out.reserve(((input.size() + 2) / 3) * 4); + size_t i = 0; + for (; i + 3 <= input.size(); i += 3) { + const uint32_t chunk = (static_cast(input[i]) << 16) | + (static_cast(input[i + 1]) << 8) | + static_cast(input[i + 2]); + out.push_back(kTable[(chunk >> 18) & 0x3f]); + out.push_back(kTable[(chunk >> 12) & 0x3f]); + out.push_back(kTable[(chunk >> 6) & 0x3f]); + out.push_back(kTable[chunk & 0x3f]); + } + const size_t remaining = input.size() - i; + if (remaining == 1) { + const uint32_t chunk = static_cast(input[i]) << 16; + out.push_back(kTable[(chunk >> 18) & 0x3f]); + out.push_back(kTable[(chunk >> 12) & 0x3f]); + out.push_back('='); + out.push_back('='); + } else if (remaining == 2) { + const uint32_t chunk = (static_cast(input[i]) << 16) | + (static_cast(input[i + 1]) << 8); + out.push_back(kTable[(chunk >> 18) & 0x3f]); + out.push_back(kTable[(chunk >> 12) & 0x3f]); + out.push_back(kTable[(chunk >> 6) & 0x3f]); + out.push_back('='); + } + return out; +} + +bool IsEvalOrPrintFlag(const std::string& arg) { + return arg == "-e" || arg == "--eval" || arg == "-p" || arg == "--print"; +} + +void RewriteWasixMultilineEvalArgs(std::vector* args) { +#ifdef __wasi__ + if (args == nullptr || args->size() < 3) return; + if (!IsEvalOrPrintFlag((*args)[1])) return; + std::string& script = (*args)[2]; + if (script.find('\n') == std::string::npos && script.find('\r') == std::string::npos) return; + script = "eval(Buffer.from('" + Base64Encode(script) + "', 'base64').toString())"; +#else + (void)args; +#endif +} + bool CoerceValueToUtf8(napi_env env, napi_value value, std::string* out) { if (out == nullptr || value == nullptr) return false; napi_value as_string = nullptr; @@ -448,6 +498,7 @@ bool ParseSpawnOptions(napi_env env, napi_value value, SpawnOptions* out) { if (out->args.empty()) { out->args.push_back(out->file); } + RewriteWasixMultilineEvalArgs(&out->args); napi_value cwd_val = nullptr; if (GetNamedProperty(env, value, "cwd", &cwd_val)) { diff --git a/src/edge_util.cc b/src/edge_util.cc index 839ef33be..2f6e2aa34 100644 --- a/src/edge_util.cc +++ b/src/edge_util.cc @@ -503,6 +503,14 @@ napi_value GuessHandleType(napi_env env, napi_callback_info info) { return Undefined(env); } uv_handle_type t = uv_guess_handle(static_cast(fd)); +#ifdef __wasi__ + if (t == UV_TTY && fd >= 0 && fd <= 2) { + struct stat fd_stat {}; + if (fstat(fd, &fd_stat) == 0) { + t = UV_NAMED_PIPE; + } + } +#endif #ifndef _WIN32 if (fd == 0 && t == UV_FILE) { struct stat fd_stat {}; @@ -534,7 +542,7 @@ napi_value IsInsideNodeModulesCallback(napi_env env, napi_callback_info info) { if (napi_typeof(env, argv[0], &type) == napi_ok && type == napi_number) { int32_t candidate = 0; if (napi_get_value_int32(env, argv[0], &candidate) == napi_ok) { - frame_limit = candidate; + frame_limit = std::max(frame_limit, candidate); } } } @@ -543,9 +551,10 @@ napi_value IsInsideNodeModulesCallback(napi_env env, napi_callback_info info) { bool result = false; uint32_t frames = static_cast(frame_limit); if (frames > kMaxCallSitesFrames) frames = kMaxCallSitesFrames; + const uint32_t raw_frames = RawCallSiteFramesForEdge(frames); napi_value callsites = nullptr; - if (unofficial_napi_get_current_stack_trace(env, frames, &callsites) == napi_ok && callsites != nullptr) { + if (unofficial_napi_get_call_sites(env, raw_frames, &callsites) == napi_ok && callsites != nullptr) { bool is_array = false; if (napi_is_array(env, callsites, &is_array) == napi_ok && is_array) { uint32_t length = 0; From f1999f45d43453d508db45b3b52e4115983e26a8 Mon Sep 17 00:00:00 2001 From: Sadhbh Code Date: Wed, 10 Jun 2026 16:10:33 +0100 Subject: [PATCH 18/65] Removed hacks: loopback, and spawn --- src/edge_cares_wrap.cc | 27 --------------------- src/edge_process_wrap.cc | 51 ---------------------------------------- src/edge_spawn_sync.cc | 50 --------------------------------------- 3 files changed, 128 deletions(-) diff --git a/src/edge_cares_wrap.cc b/src/edge_cares_wrap.cc index 99fa24b52..140486b32 100644 --- a/src/edge_cares_wrap.cc +++ b/src/edge_cares_wrap.cc @@ -1628,19 +1628,6 @@ int DispatchQuery(ChannelWrap* channel, CaresReqWrap* req, const QueryMethodData return UV_EINVAL; } - if (req->hostname == "127.0.0.1" || req->hostname == "::1") { - channel->active_reqs.insert(req); - char localhost[] = "localhost"; - char* aliases[] = {localhost, nullptr}; - hostent host{}; - host.h_name = localhost; - host.h_aliases = aliases; - host.h_addrtype = family; - host.h_length = length; - CompleteQuery(req, ARES_SUCCESS, nullptr, 0, &host, true); - return 0; - } - channel->active_reqs.insert(req); ares_gethostbyaddr(channel->channel, address_buffer, @@ -1999,20 +1986,6 @@ napi_value CaresGetNameInfo(napi_env env, napi_callback_info info) { int32_t port = 0; napi_get_value_int32(env, argv[2], &port); - if (host == "127.0.0.1" || host == "::1") { - const std::string service = std::to_string(port); - napi_value cb_argv[3] = { - MakeInt32(env, 0), - MakeStringUtf8(env, "localhost"), - MakeStringUtf8(env, service.c_str()), - }; - InvokeOnComplete(env, req, 3, cb_argv); - UntrackPendingReq(req); - MarkReqComplete(req); - CleanupReqAfterAsync(req); - return MakeInt32(env, 0); - } - sockaddr_storage storage{}; int rc = uv_ip4_addr(host.c_str(), port, reinterpret_cast(&storage)); if (rc != 0) { diff --git a/src/edge_process_wrap.cc b/src/edge_process_wrap.cc index 682f74d5f..da23368ef 100644 --- a/src/edge_process_wrap.cc +++ b/src/edge_process_wrap.cc @@ -8,7 +8,6 @@ #include #include #include -#include #include #include #include @@ -39,55 +38,6 @@ struct ProcessWrap { std::mutex g_live_child_pids_mutex; std::unordered_set g_live_child_pids; -std::string Base64Encode(std::string_view input) { - static constexpr char kTable[] = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - std::string out; - out.reserve(((input.size() + 2) / 3) * 4); - size_t i = 0; - for (; i + 3 <= input.size(); i += 3) { - const uint32_t chunk = (static_cast(input[i]) << 16) | - (static_cast(input[i + 1]) << 8) | - static_cast(input[i + 2]); - out.push_back(kTable[(chunk >> 18) & 0x3f]); - out.push_back(kTable[(chunk >> 12) & 0x3f]); - out.push_back(kTable[(chunk >> 6) & 0x3f]); - out.push_back(kTable[chunk & 0x3f]); - } - const size_t remaining = input.size() - i; - if (remaining == 1) { - const uint32_t chunk = static_cast(input[i]) << 16; - out.push_back(kTable[(chunk >> 18) & 0x3f]); - out.push_back(kTable[(chunk >> 12) & 0x3f]); - out.push_back('='); - out.push_back('='); - } else if (remaining == 2) { - const uint32_t chunk = (static_cast(input[i]) << 16) | - (static_cast(input[i + 1]) << 8); - out.push_back(kTable[(chunk >> 18) & 0x3f]); - out.push_back(kTable[(chunk >> 12) & 0x3f]); - out.push_back(kTable[(chunk >> 6) & 0x3f]); - out.push_back('='); - } - return out; -} - -bool IsEvalOrPrintFlag(const std::string& arg) { - return arg == "-e" || arg == "--eval" || arg == "-p" || arg == "--print"; -} - -void RewriteWasixMultilineEvalArgs(std::vector* args) { -#ifdef __wasi__ - if (args == nullptr || args->size() < 3) return; - if (!IsEvalOrPrintFlag((*args)[1])) return; - std::string& script = (*args)[2]; - if (script.find('\n') == std::string::npos && script.find('\r') == std::string::npos) return; - script = "eval(Buffer.from('" + Base64Encode(script) + "', 'base64').toString())"; -#else - (void)args; -#endif -} - void RefreshArgvPointers(std::vector* storage, std::vector* out) { if (storage == nullptr || out == nullptr) return; out->clear(); @@ -540,7 +490,6 @@ napi_value ProcessSpawn(napi_env env, napi_callback_info info) { if (args_storage.empty()) { args_storage.push_back(file); } - RewriteWasixMultilineEvalArgs(&args_storage); RefreshArgvPointers(&args_storage, &args); napi_value env_pairs_value = nullptr; diff --git a/src/edge_spawn_sync.cc b/src/edge_spawn_sync.cc index d9d303881..de4940a0c 100644 --- a/src/edge_spawn_sync.cc +++ b/src/edge_spawn_sync.cc @@ -134,55 +134,6 @@ std::string ValueToUtf8(napi_env env, napi_value value) { return out; } -std::string Base64Encode(std::string_view input) { - static constexpr char kTable[] = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - std::string out; - out.reserve(((input.size() + 2) / 3) * 4); - size_t i = 0; - for (; i + 3 <= input.size(); i += 3) { - const uint32_t chunk = (static_cast(input[i]) << 16) | - (static_cast(input[i + 1]) << 8) | - static_cast(input[i + 2]); - out.push_back(kTable[(chunk >> 18) & 0x3f]); - out.push_back(kTable[(chunk >> 12) & 0x3f]); - out.push_back(kTable[(chunk >> 6) & 0x3f]); - out.push_back(kTable[chunk & 0x3f]); - } - const size_t remaining = input.size() - i; - if (remaining == 1) { - const uint32_t chunk = static_cast(input[i]) << 16; - out.push_back(kTable[(chunk >> 18) & 0x3f]); - out.push_back(kTable[(chunk >> 12) & 0x3f]); - out.push_back('='); - out.push_back('='); - } else if (remaining == 2) { - const uint32_t chunk = (static_cast(input[i]) << 16) | - (static_cast(input[i + 1]) << 8); - out.push_back(kTable[(chunk >> 18) & 0x3f]); - out.push_back(kTable[(chunk >> 12) & 0x3f]); - out.push_back(kTable[(chunk >> 6) & 0x3f]); - out.push_back('='); - } - return out; -} - -bool IsEvalOrPrintFlag(const std::string& arg) { - return arg == "-e" || arg == "--eval" || arg == "-p" || arg == "--print"; -} - -void RewriteWasixMultilineEvalArgs(std::vector* args) { -#ifdef __wasi__ - if (args == nullptr || args->size() < 3) return; - if (!IsEvalOrPrintFlag((*args)[1])) return; - std::string& script = (*args)[2]; - if (script.find('\n') == std::string::npos && script.find('\r') == std::string::npos) return; - script = "eval(Buffer.from('" + Base64Encode(script) + "', 'base64').toString())"; -#else - (void)args; -#endif -} - bool CoerceValueToUtf8(napi_env env, napi_value value, std::string* out) { if (out == nullptr || value == nullptr) return false; napi_value as_string = nullptr; @@ -498,7 +449,6 @@ bool ParseSpawnOptions(napi_env env, napi_value value, SpawnOptions* out) { if (out->args.empty()) { out->args.push_back(out->file); } - RewriteWasixMultilineEvalArgs(&out->args); napi_value cwd_val = nullptr; if (GetNamedProperty(env, value, "cwd", &cwd_val)) { From dc9a046509ff36b4f1e952967528480a5873e4b2 Mon Sep 17 00:00:00 2001 From: Sadhbh Code Date: Wed, 10 Jun 2026 16:35:22 +0100 Subject: [PATCH 19/65] Added /etc/hosts, removed stream type normalization --- quickjs-wasm/etc/hosts | 2 ++ quickjs-wasm/wasmer.toml | 1 + scripts/edge-wasix-node-runner.sh | 4 ++++ src/edge_fs.cc | 12 +----------- src/edge_process_wrap.cc | 14 +++----------- src/internal_binding/binding_fs.cc | 12 +----------- 6 files changed, 12 insertions(+), 33 deletions(-) create mode 100644 quickjs-wasm/etc/hosts diff --git a/quickjs-wasm/etc/hosts b/quickjs-wasm/etc/hosts new file mode 100644 index 000000000..77c24a17a --- /dev/null +++ b/quickjs-wasm/etc/hosts @@ -0,0 +1,2 @@ +127.0.0.1 localhost +::1 localhost diff --git a/quickjs-wasm/wasmer.toml b/quickjs-wasm/wasmer.toml index eab584920..6da2ef415 100644 --- a/quickjs-wasm/wasmer.toml +++ b/quickjs-wasm/wasmer.toml @@ -14,4 +14,5 @@ name = "edge" module = "edge" [fs] +"/etc" = "./etc" "/usr/local/ssl" = "../ssl-certs" diff --git a/scripts/edge-wasix-node-runner.sh b/scripts/edge-wasix-node-runner.sh index 26908375b..359609f1d 100755 --- a/scripts/edge-wasix-node-runner.sh +++ b/scripts/edge-wasix-node-runner.sh @@ -81,6 +81,10 @@ volume_args=( --volume "${edgejs_root}/ssl-certs:/usr/local/ssl" ) +if [[ -d "${package_dir}/etc" ]]; then + volume_args+=(--volume "${package_dir}/etc:/etc") +fi + IFS=',' read -r -a workspace_dirs <<< "${workspace_dirs_csv}" for workspace_dir in "${workspace_dirs[@]}"; do workspace_dir="${workspace_dir#"${workspace_dir%%[![:space:]]*}"}" diff --git a/src/edge_fs.cc b/src/edge_fs.cc index 43d24198c..02a1165ff 100644 --- a/src/edge_fs.cc +++ b/src/edge_fs.cc @@ -813,19 +813,9 @@ napi_value BindingRealpath(napi_env env, napi_callback_info info) { // birthtime_sec, birthtime_nsec (18 elements). constexpr size_t kStatArrayLength = 18; -uint64_t NormalizeStatMode(uint64_t mode) { -#if defined(__wasi__) - if ((mode & 0777) == 0) { - if (S_ISDIR(mode)) return mode | 0700; - if (S_ISREG(mode)) return mode | 0600; - } -#endif - return mode; -} - void StatToArray(const uv_stat_t* s, double* out) { out[0] = static_cast(s->st_dev); - out[1] = static_cast(NormalizeStatMode(static_cast(s->st_mode))); + out[1] = static_cast(s->st_mode); out[2] = static_cast(s->st_nlink); out[3] = static_cast(s->st_uid); out[4] = static_cast(s->st_gid); diff --git a/src/edge_process_wrap.cc b/src/edge_process_wrap.cc index da23368ef..4741fe160 100644 --- a/src/edge_process_wrap.cc +++ b/src/edge_process_wrap.cc @@ -38,16 +38,6 @@ struct ProcessWrap { std::mutex g_live_child_pids_mutex; std::unordered_set g_live_child_pids; -void RefreshArgvPointers(std::vector* storage, std::vector* out) { - if (storage == nullptr || out == nullptr) return; - out->clear(); - out->reserve(storage->size() + 1); - for (std::string& arg : *storage) { - out->push_back(const_cast(arg.c_str())); - } - out->push_back(nullptr); -} - napi_value MakeInt32(napi_env env, int32_t value) { napi_value out = nullptr; napi_create_int32(env, value, &out); @@ -488,9 +478,11 @@ napi_value ProcessSpawn(napi_env env, napi_callback_info info) { if (!ParseStringArray(env, args_value, &args_storage, &args)) return MakeInt32(env, UV_EINVAL); } if (args_storage.empty()) { + args.clear(); args_storage.push_back(file); + args.push_back(const_cast(args_storage.back().c_str())); + args.push_back(nullptr); } - RefreshArgvPointers(&args_storage, &args); napi_value env_pairs_value = nullptr; std::vector env_storage; diff --git a/src/internal_binding/binding_fs.cc b/src/internal_binding/binding_fs.cc index c18dd40d5..e220f8f41 100644 --- a/src/internal_binding/binding_fs.cc +++ b/src/internal_binding/binding_fs.cc @@ -378,20 +378,10 @@ napi_value CreateTypedStatsArray(napi_env env, size_t length, bool as_bigint, na return out; } -uint64_t NormalizeUvMode(uint64_t mode) { -#if defined(__wasi__) - if ((mode & 0777) == 0) { - if (S_ISDIR(mode)) return mode | 0700; - if (S_ISREG(mode)) return mode | 0600; - } -#endif - return mode; -} - void PopulateStatsArrayFromUv(const uv_stat_t* stat, double* out) { if (stat == nullptr || out == nullptr) return; out[0] = static_cast(stat->st_dev); - out[1] = static_cast(NormalizeUvMode(static_cast(stat->st_mode))); + out[1] = static_cast(stat->st_mode); out[2] = static_cast(stat->st_nlink); out[3] = static_cast(stat->st_uid); out[4] = static_cast(stat->st_gid); From b9ef182f2322eb0e111f40427e4685852ec6a241 Mon Sep 17 00:00:00 2001 From: Sadhbh Code Date: Fri, 12 Jun 2026 12:53:02 +0100 Subject: [PATCH 20/65] Fixes for OpenSSL error code mapping --- .gitmodules | 3 + deps/libev-wasix | 1 + src/internal_binding/binding_crypto.cc | 197 ++++++++++++++----------- 3 files changed, 114 insertions(+), 87 deletions(-) create mode 160000 deps/libev-wasix diff --git a/.gitmodules b/.gitmodules index 063fee22b..b59ced449 100644 --- a/.gitmodules +++ b/.gitmodules @@ -18,3 +18,6 @@ [submodule "deps/libuv-wasix"] path = deps/libuv-wasix url = git@github.com:wasix-org/libuv.git +[submodule "deps/libev-wasix"] + path = deps/libev-wasix + url = git@github.com:Anodized-Titanium/libev.git diff --git a/deps/libev-wasix b/deps/libev-wasix new file mode 160000 index 000000000..93823e6ca --- /dev/null +++ b/deps/libev-wasix @@ -0,0 +1 @@ +Subproject commit 93823e6ca699df195a6c7b8bfa6006ec40ee0003 diff --git a/src/internal_binding/binding_crypto.cc b/src/internal_binding/binding_crypto.cc index 7a9af422f..5af216abe 100644 --- a/src/internal_binding/binding_crypto.cc +++ b/src/internal_binding/binding_crypto.cc @@ -16,6 +16,7 @@ #include #include +#include #include #include #include @@ -25,11 +26,16 @@ #include #include #include +#include #include #include +#include #include #include +#include +#include #include +#include #include #include "ncrypto.h" @@ -566,88 +572,71 @@ void SetErrorStringProperty(napi_env env, napi_value err, const char* name, cons const char* MapOpenSslErrorCode(unsigned long err) { if (err == 0) return nullptr; - const char* library = ERR_lib_error_string(err); - const char* reason = ERR_reason_error_string(err); - if (reason != nullptr && - std::strcmp(reason, "invalid digest") == 0) { - return "ERR_OSSL_INVALID_DIGEST"; - } - if (reason != nullptr && - std::strcmp(reason, "bad decrypt") == 0) { - return "ERR_OSSL_BAD_DECRYPT"; - } - if (reason != nullptr && - std::strcmp(reason, "operation not supported for this keytype") == 0) { - return "ERR_OSSL_EVP_OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE"; - } - if (reason != nullptr && - std::strcmp(reason, "wrong final block length") == 0) { - return "ERR_OSSL_WRONG_FINAL_BLOCK_LENGTH"; - } - if (reason != nullptr && - std::strcmp(reason, "data not multiple of block length") == 0) { - return "ERR_OSSL_DATA_NOT_MULTIPLE_OF_BLOCK_LENGTH"; - } - if (reason != nullptr && - std::strcmp(reason, "bignum too long") == 0) { - return "ERR_OSSL_BN_BIGNUM_TOO_LONG"; - } - if (reason != nullptr && - std::strcmp(reason, "missing OID") == 0) { - return "ERR_OSSL_MISSING_OID"; - } - if (reason != nullptr && - std::strcmp(reason, "modulus too small") == 0) { - return "ERR_OSSL_DH_MODULUS_TOO_SMALL"; - } - if (reason != nullptr && - std::strcmp(reason, "bits too small") == 0) { - return "ERR_OSSL_BN_BITS_TOO_SMALL"; - } - if (reason != nullptr && - std::strcmp(reason, "bad generator") == 0) { - return "ERR_OSSL_DH_BAD_GENERATOR"; - } - if (reason != nullptr && - std::strcmp(reason, "mismatching domain parameters") == 0) { - return "ERR_OSSL_MISMATCHING_DOMAIN_PARAMETERS"; - } - if (reason != nullptr && - std::strcmp(reason, "different parameters") == 0) { - return "ERR_OSSL_EVP_DIFFERENT_PARAMETERS"; - } - if (reason != nullptr && - std::strcmp(reason, "interrupted or cancelled") == 0) { - return "ERR_OSSL_CRYPTO_INTERRUPTED_OR_CANCELLED"; - } - if (library != nullptr && - reason != nullptr && - std::strcmp(library, "DECODER routines") == 0 && - std::strcmp(reason, "unsupported") == 0) { - return "ERR_OSSL_UNSUPPORTED"; - } + switch (ERR_PACK(ERR_GET_LIB(err), 0, ERR_GET_REASON(err))) { + case ERR_PACK(ERR_LIB_EVP, 0, EVP_R_INVALID_DIGEST): + case ERR_PACK(ERR_LIB_RSA, 0, RSA_R_INVALID_DIGEST): + case ERR_PACK(ERR_LIB_PROV, 0, PROV_R_INVALID_DIGEST): + return "ERR_OSSL_INVALID_DIGEST"; + case ERR_PACK(ERR_LIB_EVP, 0, EVP_R_BAD_DECRYPT): + case ERR_PACK(ERR_LIB_PROV, 0, PROV_R_BAD_DECRYPT): + case ERR_PACK(ERR_LIB_PEM, 0, PEM_R_BAD_DECRYPT): + return "ERR_OSSL_BAD_DECRYPT"; + case ERR_PACK(ERR_LIB_EVP, 0, EVP_R_OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE): + case ERR_PACK(ERR_LIB_RSA, 0, RSA_R_OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE): + case ERR_PACK(ERR_LIB_PROV, 0, PROV_R_OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE): + return "ERR_OSSL_EVP_OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE"; + case ERR_PACK(ERR_LIB_EVP, 0, EVP_R_COMMAND_NOT_SUPPORTED): + return "ERR_OSSL_EVP_COMMAND_NOT_SUPPORTED"; + case ERR_PACK(ERR_LIB_EVP, 0, EVP_R_DECODE_ERROR): + return "ERR_OSSL_EVP_DECODE_ERROR"; + case ERR_PACK(ERR_LIB_ASN1, 0, ASN1_R_DECODE_ERROR): + return "ERR_OSSL_ASN1_DECODE_ERROR"; + case ERR_PACK(ERR_LIB_ASN1, 0, ASN1_R_WRONG_TAG): + return "ERR_OSSL_ASN1_WRONG_TAG"; + case ERR_PACK(ERR_LIB_EVP, 0, EVP_R_WRONG_FINAL_BLOCK_LENGTH): + case ERR_PACK(ERR_LIB_PROV, 0, PROV_R_WRONG_FINAL_BLOCK_LENGTH): + return "ERR_OSSL_WRONG_FINAL_BLOCK_LENGTH"; + case ERR_PACK(ERR_LIB_EVP, 0, EVP_R_DATA_NOT_MULTIPLE_OF_BLOCK_LENGTH): + return "ERR_OSSL_DATA_NOT_MULTIPLE_OF_BLOCK_LENGTH"; + case ERR_PACK(ERR_LIB_BN, 0, BN_R_BIGNUM_TOO_LONG): + return "ERR_OSSL_BN_BIGNUM_TOO_LONG"; + case ERR_PACK(ERR_LIB_EC, 0, EC_R_MISSING_OID): + case ERR_PACK(ERR_LIB_PROV, 0, PROV_R_MISSING_OID): + return "ERR_OSSL_MISSING_OID"; + case ERR_PACK(ERR_LIB_DH, 0, DH_R_MODULUS_TOO_SMALL): + return "ERR_OSSL_DH_MODULUS_TOO_SMALL"; + case ERR_PACK(ERR_LIB_BN, 0, BN_R_BITS_TOO_SMALL): + return "ERR_OSSL_BN_BITS_TOO_SMALL"; + case ERR_PACK(ERR_LIB_DH, 0, DH_R_BAD_GENERATOR): + return "ERR_OSSL_DH_BAD_GENERATOR"; + case ERR_PACK(ERR_LIB_PROV, 0, PROV_R_MISMATCHING_DOMAIN_PARAMETERS): + return "ERR_OSSL_MISMATCHING_DOMAIN_PARAMETERS"; + case ERR_PACK(ERR_LIB_EVP, 0, EVP_R_DIFFERENT_PARAMETERS): + return "ERR_OSSL_EVP_DIFFERENT_PARAMETERS"; + case ERR_PACK(ERR_LIB_EVP, 0, ERR_R_INTERRUPTED_OR_CANCELLED): + return "ERR_OSSL_CRYPTO_INTERRUPTED_OR_CANCELLED"; + case ERR_PACK(ERR_LIB_RSA, 0, RSA_R_ILLEGAL_OR_UNSUPPORTED_PADDING_MODE): + return "ERR_OSSL_RSA_ILLEGAL_OR_UNSUPPORTED_PADDING_MODE"; + case ERR_PACK(ERR_LIB_PROV, 0, PROV_R_ILLEGAL_OR_UNSUPPORTED_PADDING_MODE): + return "ERR_OSSL_ILLEGAL_OR_UNSUPPORTED_PADDING_MODE"; + case ERR_PACK(ERR_LIB_EVP, 0, EVP_R_UNSUPPORTED_ALGORITHM): + return "ERR_OSSL_EVP_UNSUPPORTED_ALGORITHM"; + case ERR_PACK(ERR_LIB_EVP, 0, EVP_R_UNSUPPORTED_KEY_TYPE): + return "ERR_OSSL_EVP_UNSUPPORTED_KEY_TYPE"; + case ERR_PACK(ERR_LIB_OSSL_DECODER, 0, ERR_R_UNSUPPORTED): + return "ERR_OSSL_UNSUPPORTED"; #if OPENSSL_VERSION_NUMBER < 0x30000000L - const char* function = ERR_func_error_string(err); - if (library != nullptr && - reason != nullptr && - function != nullptr && - std::strcmp(library, "PEM routines") == 0 && - std::strcmp(function, "get_name") == 0 && - std::strcmp(reason, "no start line") == 0) { - return "ERR_OSSL_PEM_NO_START_LINE"; - } + case ERR_PACK(ERR_LIB_PEM, 0, PEM_R_NO_START_LINE): + return "ERR_OSSL_PEM_NO_START_LINE"; #endif + } return nullptr; } bool IsOpenSslDecoderUnsupportedError(unsigned long err) { if (err == 0) return false; - const char* library = ERR_lib_error_string(err); - const char* reason = ERR_reason_error_string(err); - return library != nullptr && - reason != nullptr && - std::strcmp(library, "DECODER routines") == 0 && - std::strcmp(reason, "unsupported") == 0; + return ERR_GET_LIB(err) == ERR_LIB_OSSL_DECODER && + ERR_GET_REASON(err) == ERR_R_UNSUPPORTED; } const char* MapOpenSslKeyParseErrorCode(unsigned long err, bool require_private) { @@ -657,6 +646,21 @@ const char* MapOpenSslKeyParseErrorCode(unsigned long err, bool require_private) return MapOpenSslErrorCode(err); } +int OpenSslMappedErrorPriority(unsigned long err, bool require_private) { + if (err == 0) return 0; + if (!require_private && IsOpenSslDecoderUnsupportedError(err)) return 3; + + switch (ERR_PACK(ERR_GET_LIB(err), 0, ERR_GET_REASON(err))) { + case ERR_PACK(ERR_LIB_EVP, 0, EVP_R_DECODE_ERROR): + return 3; + case ERR_PACK(ERR_LIB_ASN1, 0, ASN1_R_DECODE_ERROR): + case ERR_PACK(ERR_LIB_ASN1, 0, ASN1_R_WRONG_TAG): + return 1; + } + + return MapOpenSslErrorCode(err) != nullptr ? 2 : 0; +} + napi_value CreateOpenSslError(napi_env env, const char* code, unsigned long err, @@ -724,10 +728,35 @@ void ThrowLastOpenSslMessage(napi_env env, const char* fallback_message) { } unsigned long ConsumePreferredOpenSslError() { - const unsigned long selected = ERR_get_error(); - while (ERR_get_error() != 0) { + unsigned long first = 0; + unsigned long selected = 0; + int selected_priority = 0; + unsigned long err = 0; + while ((err = ERR_get_error()) != 0) { + if (first == 0) first = err; + const int priority = OpenSslMappedErrorPriority(err, true); + if (priority > selected_priority) { + selected = err; + selected_priority = priority; + } + } + return selected != 0 ? selected : first; +} + +unsigned long ConsumePreferredOpenSslKeyParseError(bool require_private) { + unsigned long first = 0; + unsigned long selected = 0; + int selected_priority = 0; + unsigned long err = 0; + while ((err = ERR_get_error()) != 0) { + if (first == 0) first = err; + const int priority = OpenSslMappedErrorPriority(err, require_private); + if (priority > selected_priority) { + selected = err; + selected_priority = priority; + } } - return selected; + return selected != 0 ? selected : first; } constexpr unsigned long kOpenSslDecoderUnsupportedError = 0x1E08010CUL; @@ -753,9 +782,7 @@ void SetPreferredOpenSslError(std::string* code, const char* fallback_message) { if (code == nullptr || message == nullptr) return; - const unsigned long selected = ERR_get_error(); - while (ERR_get_error() != 0) { - } + const unsigned long selected = ConsumePreferredOpenSslError(); if (selected != 0) { if (const char* mapped = MapOpenSslErrorCode(selected)) { @@ -777,9 +804,7 @@ void SetPreferredOpenSslError(std::string* code, } void ThrowLastOpenSslKeyParseError(napi_env env, bool require_private, const char* fallback_message) { - const unsigned long selected = ERR_get_error(); - while (ERR_get_error() != 0) { - } + const unsigned long selected = ConsumePreferredOpenSslKeyParseError(require_private); if (selected == 0) { napi_throw_error(env, nullptr, fallback_message); return; @@ -796,9 +821,7 @@ void SetPreferredOpenSslKeyParseError(std::string* code, const char* fallback_message) { if (code == nullptr || message == nullptr) return; - const unsigned long selected = ERR_get_error(); - while (ERR_get_error() != 0) { - } + const unsigned long selected = ConsumePreferredOpenSslKeyParseError(require_private); if (selected != 0) { if (const char* mapped = MapOpenSslKeyParseErrorCode(selected, require_private)) { From 5254f803ff93200d79231d87501f813eb1b35147 Mon Sep 17 00:00:00 2001 From: Sadhbh Code Date: Fri, 12 Jun 2026 17:38:43 +0100 Subject: [PATCH 21/65] OpenSSL update to 3.5 --- .gitmodules | 7 ++++--- CMakeLists.txt | 4 ++-- deps/libuv-wasix | 2 +- deps/openssl-wasix | 1 + quickjs-wasm/build.sh | 14 ++++++++++++-- wasix/build-wasix.sh | 14 ++++++++++++-- wasix/setup-wasix-deps.sh | 40 ++++++++++----------------------------- 7 files changed, 42 insertions(+), 40 deletions(-) create mode 160000 deps/openssl-wasix diff --git a/.gitmodules b/.gitmodules index b59ced449..75c7e4796 100644 --- a/.gitmodules +++ b/.gitmodules @@ -18,6 +18,7 @@ [submodule "deps/libuv-wasix"] path = deps/libuv-wasix url = git@github.com:wasix-org/libuv.git -[submodule "deps/libev-wasix"] - path = deps/libev-wasix - url = git@github.com:Anodized-Titanium/libev.git +[submodule "deps/openssl-wasix"] + path = deps/openssl-wasix + url = git@github.com:Anodized-Titanium/openssl.git + branch = upstream-35-merge diff --git a/CMakeLists.txt b/CMakeLists.txt index 52524a9a4..2a9698b57 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -418,13 +418,13 @@ target_compile_definitions(edge_nghttp2 if(EDGE_IS_WASIX_TARGET) set(EDGE_OPENSSL_WASIX_ROOT "${PROJECT_ROOT}/deps/openssl-wasix" - CACHE PATH "Path to wasix-org OpenSSL source/build tree" + CACHE PATH "Path to WASIX OpenSSL source/build tree" ) set(_edge_openssl_include_dir "${EDGE_OPENSSL_WASIX_ROOT}/include") if(NOT EXISTS "${_edge_openssl_include_dir}") message(FATAL_ERROR "OpenSSL include directory not found: ${_edge_openssl_include_dir}. " - "For WASIX, clone deps with wasix/setup-wasix-deps.sh.") + "For WASIX, run git submodule update --init deps/openssl-wasix.") endif() set(_edge_openssl_crypto "") diff --git a/deps/libuv-wasix b/deps/libuv-wasix index 5539194c8..0ae770b9d 160000 --- a/deps/libuv-wasix +++ b/deps/libuv-wasix @@ -1 +1 @@ -Subproject commit 5539194c8f9e751e4a7da94cd4fb69722736e9ae +Subproject commit 0ae770b9daae35d97d3c9a2693a5b22362124f12 diff --git a/deps/openssl-wasix b/deps/openssl-wasix new file mode 160000 index 000000000..a8632cb81 --- /dev/null +++ b/deps/openssl-wasix @@ -0,0 +1 @@ +Subproject commit a8632cb81bc5a62f77f6286019ac922d1cd8e270 diff --git a/quickjs-wasm/build.sh b/quickjs-wasm/build.sh index 3835619e9..6787c6dde 100755 --- a/quickjs-wasm/build.sh +++ b/quickjs-wasm/build.sh @@ -133,7 +133,17 @@ if [[ -d "${BUILD_DIR}/CMakeFiles" ]]; then rm -rf "${BUILD_DIR}/CMakeFiles" fi -if [[ ! -f "${OPENSSL_WASIX_DIR}/libcrypto.a" || ! -f "${OPENSSL_WASIX_DIR}/libssl.a" ]]; then +openssl_wasix_ready() { + [[ -f "${OPENSSL_WASIX_DIR}/libcrypto.a" ]] && + [[ -f "${OPENSSL_WASIX_DIR}/libssl.a" ]] && + [[ -f "${OPENSSL_WASIX_DIR}/include/openssl/ssl.h" ]] && + [[ -f "${OPENSSL_WASIX_DIR}/include/openssl/comp.h" ]] && + [[ -f "${OPENSSL_WASIX_DIR}/include/openssl/e_ostime.h" ]] && + [[ -f "${OPENSSL_WASIX_DIR}/include/openssl/lhash.h" ]] && + [[ -f "${OPENSSL_WASIX_DIR}/include/openssl/opensslv.h" ]] +} + +if ! openssl_wasix_ready; then echo "Building OpenSSL static libraries for WASIX..." ( cd "${OPENSSL_WASIX_DIR}" @@ -146,7 +156,7 @@ if [[ ! -f "${OPENSSL_WASIX_DIR}/libcrypto.a" || ! -f "${OPENSSL_WASIX_DIR}/libs LD=wasixld \ CFLAGS="--target=wasm32-wasix -matomics -mbulk-memory -mmutable-globals -pthread -mthread-model posix -ftls-model=local-exec -fno-trapping-math -D_WASI_EMULATED_MMAN -D_WASI_EMULATED_SIGNAL -D_WASI_EMULATED_PROCESS_CLOCKS -DUSE_TIMEGM -DOPENSSL_NO_SECURE_MEMORY -DOPENSSL_NO_DGRAM -DOPENSSL_THREADS -O2" \ LDFLAGS="-Wl,--allow-undefined" \ - ./Configure linux-generic32 -static no-shared no-pic no-asm no-tests no-apps no-afalgeng -DUSE_TIMEGM -DOPENSSL_NO_SECURE_MEMORY -DOPENSSL_NO_DGRAM -DOPENSSL_THREADS + ./Configure linux-generic32 -static no-shared no-pic no-asm no-dso no-tests no-apps no-afalgeng -DUSE_TIMEGM -DOPENSSL_NO_SECURE_MEMORY -DOPENSSL_NO_DGRAM -DOPENSSL_THREADS make build_generated make -j4 libcrypto.a libssl.a wasixranlib libcrypto.a || true diff --git a/wasix/build-wasix.sh b/wasix/build-wasix.sh index 41eb10b55..76da7ce87 100755 --- a/wasix/build-wasix.sh +++ b/wasix/build-wasix.sh @@ -49,7 +49,17 @@ if [[ -d "${BUILD_DIR}/CMakeFiles" ]]; then rm -rf "${BUILD_DIR}/CMakeFiles" fi -if [[ ! -f "${OPENSSL_WASIX_DIR}/libcrypto.a" || ! -f "${OPENSSL_WASIX_DIR}/libssl.a" ]]; then +openssl_wasix_ready() { + [[ -f "${OPENSSL_WASIX_DIR}/libcrypto.a" ]] && + [[ -f "${OPENSSL_WASIX_DIR}/libssl.a" ]] && + [[ -f "${OPENSSL_WASIX_DIR}/include/openssl/ssl.h" ]] && + [[ -f "${OPENSSL_WASIX_DIR}/include/openssl/comp.h" ]] && + [[ -f "${OPENSSL_WASIX_DIR}/include/openssl/e_ostime.h" ]] && + [[ -f "${OPENSSL_WASIX_DIR}/include/openssl/lhash.h" ]] && + [[ -f "${OPENSSL_WASIX_DIR}/include/openssl/opensslv.h" ]] +} + +if ! openssl_wasix_ready; then echo "Building OpenSSL static libraries for WASIX..." ( cd "${OPENSSL_WASIX_DIR}" @@ -62,7 +72,7 @@ if [[ ! -f "${OPENSSL_WASIX_DIR}/libcrypto.a" || ! -f "${OPENSSL_WASIX_DIR}/libs LD=wasixld \ CFLAGS="--target=wasm32-wasix -matomics -mbulk-memory -mmutable-globals -pthread -mthread-model posix -ftls-model=local-exec -fno-trapping-math -D_WASI_EMULATED_MMAN -D_WASI_EMULATED_SIGNAL -D_WASI_EMULATED_PROCESS_CLOCKS -DUSE_TIMEGM -DOPENSSL_NO_SECURE_MEMORY -DOPENSSL_NO_DGRAM -DOPENSSL_THREADS -O2" \ LDFLAGS="-Wl,--allow-undefined" \ - ./Configure linux-generic32 -static no-shared no-pic no-asm no-tests no-apps no-afalgeng -DUSE_TIMEGM -DOPENSSL_NO_SECURE_MEMORY -DOPENSSL_NO_DGRAM -DOPENSSL_THREADS + ./Configure linux-generic32 -static no-shared no-pic no-asm no-dso no-tests no-apps no-afalgeng -DUSE_TIMEGM -DOPENSSL_NO_SECURE_MEMORY -DOPENSSL_NO_DGRAM -DOPENSSL_THREADS make build_generated make -j4 libcrypto.a libssl.a wasixranlib libcrypto.a || true diff --git a/wasix/setup-wasix-deps.sh b/wasix/setup-wasix-deps.sh index 128f1ee06..8cd3d0509 100755 --- a/wasix/setup-wasix-deps.sh +++ b/wasix/setup-wasix-deps.sh @@ -4,37 +4,17 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" DEPS_DIR="${PROJECT_ROOT}/deps" +OPENSSL_WASIX_DIR="${DEPS_DIR}/openssl-wasix" -clone_or_update() { - local path="$1" - local url="$2" - local branch="$3" +if [[ ! -e "${OPENSSL_WASIX_DIR}/.git" ]]; then + echo "error: ${OPENSSL_WASIX_DIR} is not initialized" >&2 + echo "Run: git submodule update --init deps/openssl-wasix" >&2 + exit 1 +fi - if [[ -e "${path}" && ! -d "${path}/.git" ]]; then - echo "error: ${path} exists but is not a git repository" >&2 - exit 1 - fi - - if [[ ! -d "${path}/.git" ]]; then - echo "Cloning ${url} (${branch}) -> ${path}" - git clone --depth 1 --branch "${branch}" "${url}" "${path}" - return - fi - - echo "Updating ${path} (${branch})" - git -C "${path}" remote set-url origin "${url}" - git -C "${path}" fetch --depth 1 origin "${branch}" - if git -C "${path}" show-ref --verify --quiet "refs/heads/${branch}"; then - git -C "${path}" checkout "${branch}" - else - git -C "${path}" checkout -b "${branch}" "origin/${branch}" - fi - if ! git -C "${path}" merge --ff-only "origin/${branch}"; then - echo "warning: ${path} has local changes; skipping fast-forward merge" >&2 - fi -} - -mkdir -p "${DEPS_DIR}" -clone_or_update "${DEPS_DIR}/openssl-wasix" "https://github.com/wasix-org/openssl.git" "master" +if [[ ! -x "${OPENSSL_WASIX_DIR}/Configure" ]]; then + echo "error: ${OPENSSL_WASIX_DIR}/Configure is missing or not executable" >&2 + exit 1 +fi echo "WASIX deps are ready under ${DEPS_DIR}" From 88e41ab06ce172c6c53a631b3147b06266428018 Mon Sep 17 00:00:00 2001 From: Sadhbh Code Date: Fri, 12 Jun 2026 17:41:50 +0100 Subject: [PATCH 22/65] Removed accidental inclusion of libev --- deps/libev-wasix | 1 - 1 file changed, 1 deletion(-) delete mode 160000 deps/libev-wasix diff --git a/deps/libev-wasix b/deps/libev-wasix deleted file mode 160000 index 93823e6ca..000000000 --- a/deps/libev-wasix +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 93823e6ca699df195a6c7b8bfa6006ec40ee0003 From 24936fe5f612f812c4154a36a251dbeff9585b2a Mon Sep 17 00:00:00 2001 From: Sadhbh Code Date: Fri, 12 Jun 2026 23:12:15 +0100 Subject: [PATCH 23/65] Plan for WASIX added --- ..._lifecycle_and_address_family_selection.md | 66 ++++++++++ ...te_files_instead_of_hard_coding_answers.md | 54 ++++++++ ...orkarounds_after_lower_layer_plans_land.md | 44 +++++++ ...sl_error_mapping_and_version_capability.md | 56 +++++++++ ...5_runner_determinism_and_workflow_setup.md | 56 +++++++++ plans/wasix-plan/edgejs/README.md | 7 ++ plans/wasix-plan/index.md | 99 +++++++++++++++ .../01_use_posix_spawn_on_wasix.md | 60 +++++++++ ...nect_and_multicast_option_compatibility.md | 53 ++++++++ .../03_tcp_keepalive_timing_options.md | 51 ++++++++ plans/wasix-plan/libuv-wasix/README.md | 5 + ...ured_clone_serdes_for_sharedarraybuffer.md | 82 +++++++++++++ .../napi/02_quickjs_napi_callsite_frames.md | 36 ++++++ plans/wasix-plan/napi/README.md | 4 + ...1_remaining_unsatisfied_networking_plan.md | 20 +++ plans/wasix-plan/networking/README.md | 3 + ...ack_limit_should_match_non_wasi_quickjs.md | 72 +++++++++++ plans/wasix-plan/quickjs/README.md | 3 + ...pe_flags_sock_nonblock_and_sock_cloexec.md | 63 ++++++++++ ..._boolean_socket_option_pointer_handling.md | 58 +++++++++ .../wasix-libc/03_getsockopt_so_error.md | 36 ++++++ ...posix_st_mode_for_files_and_directories.md | 63 ++++++++++ .../wasix-libc/05_loopback_reverse_lookup.md | 54 ++++++++ plans/wasix-plan/wasix-libc/README.md | 7 ++ ..._datagram_receive_readiness_and_backlog.md | 116 ++++++++++++++++++ ...nected_udp_send_peer_state_and_emsgsize.md | 74 +++++++++++ .../wasmer/03_last_socket_error_so_error.md | 69 +++++++++++ ...oc_spawn3_proc_exec4_for_real_argv_envp.md | 83 +++++++++++++ .../wasmer/05_partial_success_for_fd_write.md | 53 ++++++++ ...msg_recvmsg_control_data_and_fd_passing.md | 62 ++++++++++ plans/wasix-plan/wasmer/README.md | 8 ++ 31 files changed, 1517 insertions(+) create mode 100644 plans/wasix-plan/edgejs/01_c_ares_request_lifecycle_and_address_family_selection.md create mode 100644 plans/wasix-plan/edgejs/02_mount_resolver_and_certificate_files_instead_of_hard_coding_answers.md create mode 100644 plans/wasix-plan/edgejs/03_remove_wasix_only_guest_workarounds_after_lower_layer_plans_land.md create mode 100644 plans/wasix-plan/edgejs/04_openssl_error_mapping_and_version_capability.md create mode 100644 plans/wasix-plan/edgejs/05_runner_determinism_and_workflow_setup.md create mode 100644 plans/wasix-plan/edgejs/README.md create mode 100644 plans/wasix-plan/index.md create mode 100644 plans/wasix-plan/libuv-wasix/01_use_posix_spawn_on_wasix.md create mode 100644 plans/wasix-plan/libuv-wasix/02_udp_disconnect_and_multicast_option_compatibility.md create mode 100644 plans/wasix-plan/libuv-wasix/03_tcp_keepalive_timing_options.md create mode 100644 plans/wasix-plan/libuv-wasix/README.md create mode 100644 plans/wasix-plan/napi/01_quickjs_structured_clone_serdes_for_sharedarraybuffer.md create mode 100644 plans/wasix-plan/napi/02_quickjs_napi_callsite_frames.md create mode 100644 plans/wasix-plan/napi/README.md create mode 100644 plans/wasix-plan/networking/01_remaining_unsatisfied_networking_plan.md create mode 100644 plans/wasix-plan/networking/README.md create mode 100644 plans/wasix-plan/quickjs/01_wasi_stack_limit_should_match_non_wasi_quickjs.md create mode 100644 plans/wasix-plan/quickjs/README.md create mode 100644 plans/wasix-plan/wasix-libc/01_socket_type_flags_sock_nonblock_and_sock_cloexec.md create mode 100644 plans/wasix-plan/wasix-libc/02_boolean_socket_option_pointer_handling.md create mode 100644 plans/wasix-plan/wasix-libc/03_getsockopt_so_error.md create mode 100644 plans/wasix-plan/wasix-libc/04_posix_st_mode_for_files_and_directories.md create mode 100644 plans/wasix-plan/wasix-libc/05_loopback_reverse_lookup.md create mode 100644 plans/wasix-plan/wasix-libc/README.md create mode 100644 plans/wasix-plan/wasmer/01_udp_datagram_receive_readiness_and_backlog.md create mode 100644 plans/wasix-plan/wasmer/02_connected_udp_send_peer_state_and_emsgsize.md create mode 100644 plans/wasix-plan/wasmer/03_last_socket_error_so_error.md create mode 100644 plans/wasix-plan/wasmer/04_proc_spawn3_proc_exec4_for_real_argv_envp.md create mode 100644 plans/wasix-plan/wasmer/05_partial_success_for_fd_write.md create mode 100644 plans/wasix-plan/wasmer/06_future_sendmsg_recvmsg_control_data_and_fd_passing.md create mode 100644 plans/wasix-plan/wasmer/README.md diff --git a/plans/wasix-plan/edgejs/01_c_ares_request_lifecycle_and_address_family_selection.md b/plans/wasix-plan/edgejs/01_c_ares_request_lifecycle_and_address_family_selection.md new file mode 100644 index 000000000..dd00f3559 --- /dev/null +++ b/plans/wasix-plan/edgejs/01_c_ares_request_lifecycle_and_address_family_selection.md @@ -0,0 +1,66 @@ +# c-ares Request Lifecycle and Address Family Selection + +Why this is a problem: + +The c-ares binding owns request lifecycle. If a resolver channel is cancelled, +still-active requests must complete with cancellation rather than vanishing. +When callers request IPv4 or IPv6, EdgeJS must pass that family through +consistently. + +When it occurs: + +- `test-dns-channel-cancel`; +- `test-dns-channel-cancel-promise`; +- `test-dns-cancel-reverse-lookup`; +- `test-dns-multi-channel`; +- `test-http-autoselectfamily`. + +Minimal manifestation: + +```js +const dns = require('node:dns'); +const r = new dns.Resolver(); + +r.resolve4('example.com', (err) => console.log(err && err.code)); +r.cancel(); // callback must still run with ECANCELLED +``` + +Boundary: + +```text +Node dns.Resolver + -> EdgeJS c-ares binding + -> c-ares channel/request objects + -> UDP sockets via libuv/wasix-libc/Wasmer +``` + +Proposed solution: + +Track active requests by c-ares channel. On cancellation, complete pending +requests with `ECANCELLED`. Respect the family requested by JavaScript when +building `GetAddrInfo` hints. + +Sketch: + +```cpp +struct ChannelState { + std::unordered_set active; +}; + +void Cancel(ChannelState* channel) { + for (Request* req : channel->active) + req->Complete(ARES_ECANCELLED); + channel->active.clear(); +} +``` + +## Proposed Solution References + +Proposed solution can be found in: + +**Reference commits:** + +```text +edgejs 929a9151 c-ares requests are now tracked per channel... +edgejs 64faa6ca Respect address family (IPv4 or IPv6) when asked GetAddrInfo +``` diff --git a/plans/wasix-plan/edgejs/02_mount_resolver_and_certificate_files_instead_of_hard_coding_answers.md b/plans/wasix-plan/edgejs/02_mount_resolver_and_certificate_files_instead_of_hard_coding_answers.md new file mode 100644 index 000000000..14c28da5b --- /dev/null +++ b/plans/wasix-plan/edgejs/02_mount_resolver_and_certificate_files_instead_of_hard_coding_answers.md @@ -0,0 +1,54 @@ +# Mount Resolver and Certificate Files Instead of Hard-Coding Answers + +Why this is a problem: + +The guest needs normal files such as `/etc/hosts` and SSL certs. Hard-coding +loopback answers in EdgeJS or synthesizing files in Wasmer hides a missing +package/runtime configuration problem. + +When it occurs: + +- DNS loopback and `localhost` tests; +- TLS tests needing CA/cert fixtures; +- package execution in GitHub where host paths differ from local paths. + +Minimal manifestation: + +```js +require('node:dns').lookup('localhost', console.log); +require('node:https').get('https://localhost:8443/', console.log); +``` + +Proposed solution: + +Add real package files and mount them with `wasmer.toml` or the test runner: + +```toml +[[command]] +name = "edge" +module = "edgejs" + +[[command.volumes]] +source = "./etc" +target = "/etc" +``` + +Runner sketch: + +```sh +wasmer run --net \ + --volume "$EDGEJS_ROOT/quickjs-wasm/etc:/etc" \ + --volume "$EDGEJS_ROOT/ssl-certs:/usr/local/ssl" \ + "$WASIX_EDGEJS_PACKAGE_DIR" -- "$test" +``` + +## Proposed Solution References + +Proposed solution can be found in: + +**Reference commits:** + +```text +edgejs dc9a0465 Added /etc/hosts, removed stream type normalization +edgejs 38e01f79 Napi/Qjs: SharedArrayBuffer. Ssl certs in wasix tests. +``` diff --git a/plans/wasix-plan/edgejs/03_remove_wasix_only_guest_workarounds_after_lower_layer_plans_land.md b/plans/wasix-plan/edgejs/03_remove_wasix_only_guest_workarounds_after_lower_layer_plans_land.md new file mode 100644 index 000000000..cbadef3a3 --- /dev/null +++ b/plans/wasix-plan/edgejs/03_remove_wasix_only_guest_workarounds_after_lower_layer_plans_land.md @@ -0,0 +1,44 @@ +# Remove WASIX-Only Guest Workarounds After Lower-Layer Plans Land + +Why this is a problem: + +Workarounds inside EdgeJS can make the guest pass one test while hiding a broken +WASIX or libc contract. For example, base64-wrapping `edge -e` scripts in +EdgeJS avoids newline-splitting but leaves every other WASIX program with broken +argv. + +When it occurs: + +- multiline `-e` spawn; +- loopback resolver shortcuts; +- manual stat mode normalization; +- stream type normalization. + +Minimal manifestation: + +```cpp +// Avoid this as final design: +if (host == "127.0.0.1") + return "localhost"; + +// Avoid this as final design: +script = "eval(Buffer.from('" + Base64Encode(script) + "', 'base64').toString())"; +``` + +Proposed solution: + +Keep temporary workarounds only while proving a cause. Once `wasix-libc` or +Wasmer owns the missing behavior, remove EdgeJS special cases and let EdgeJS use +normal POSIX-facing APIs. + +## Proposed Solution References + +Proposed solution can be found in: + +**Reference commits:** + +```text +edgejs f1999f45 Removed hacks: loopback, and spawn +edgejs 27118329 Fixes around edge process wrap, plus w/a for edge -e eval +edgejs 61ba9604 various fixes +``` diff --git a/plans/wasix-plan/edgejs/04_openssl_error_mapping_and_version_capability.md b/plans/wasix-plan/edgejs/04_openssl_error_mapping_and_version_capability.md new file mode 100644 index 000000000..e9a1fb89b --- /dev/null +++ b/plans/wasix-plan/edgejs/04_openssl_error_mapping_and_version_capability.md @@ -0,0 +1,56 @@ +# OpenSSL Error Mapping and Version Capability + +Why this is a problem: + +Node tests often assert specific OpenSSL-backed error classes and messages. If +the binding maps the wrong item from the OpenSSL error stack, JavaScript gets a +different code from native Node. Some crypto/PQC tests also require OpenSSL 3.5 +capability. + +When it occurs: + +- TLS parser/error tests; +- crypto key generation tests; +- PQC algorithm tests. + +Minimal manifestation: + +```js +const tls = require('node:tls'); +const s = tls.connect({ port: 1 }); +s.on('error', (err) => console.log(err.code, err.message)); +``` + +Boundary: + +```text +OpenSSL C API + -> EdgeJS crypto/tls binding + -> Node Error object + -> JS test assertion +``` + +Proposed solution: + +Use OpenSSL 3.5 for the WASIX OpenSSL dependency and map OpenSSL error stack +entries into Node-visible errors using the same reason/code selection native +Node expects. + +Sketch: + +```cpp +unsigned long err = ERR_peek_last_error(); +int reason = ERR_GET_REASON(err); +return MakeNodeCryptoError(env, reason); +``` + +## Proposed Solution References + +Proposed solution can be found in: + +**Reference commits:** + +```text +edgejs 5254f803 OpenSSL update to 3.5 +edgejs b9ef182f Fixes for OpenSSL error code mapping +``` diff --git a/plans/wasix-plan/edgejs/05_runner_determinism_and_workflow_setup.md b/plans/wasix-plan/edgejs/05_runner_determinism_and_workflow_setup.md new file mode 100644 index 000000000..ebc342ded --- /dev/null +++ b/plans/wasix-plan/edgejs/05_runner_determinism_and_workflow_setup.md @@ -0,0 +1,56 @@ +# Runner Determinism and Workflow Setup + +Why this is a problem: + +The test signal is meaningless if GitHub and local runs use different Wasmer +versions, different sysroots, different compiler backends, or broad host-path +mounts. WASIX behavior can change with libc and runtime versions. + +Minimal manifestation: + +```sh +make test-wasix-quickjs-only +``` + +If the runner uses a different runtime backend or stale sysroot, failures can +look like runtime regressions when they are build-environment drift. + +Proposed solution: + +- Install the intended Wasmer version explicitly. +- Install the intended `wasixcc` and sysroot. +- Force `wasmer run --llvm` for this lane. +- Mount only the directories required by the test package. +- Preserve `HOME=/tmp`, `NODE_TEST_DIR=/workspace/test`, SSL cert volume, and + `/etc` package volume. + +Sketch: + +```sh +curl https://get.wasmer.io -sSfL | sh -s "v7.2.0-alpha.3" + +wasmer run --llvm --net \ + --env HOME=/tmp \ + --env NODE_TEST_DIR=/workspace/test \ + --volume "$EDGEJS_ROOT:/workspace" \ + --volume "$EDGEJS_ROOT/quickjs-wasm/etc:/etc" \ + --volume "$EDGEJS_ROOT/ssl-certs:/usr/local/ssl" \ + --cwd /workspace \ + "$EDGEJS_ROOT/quickjs-wasm" -- "$test" +``` + +## Proposed Solution References + +Proposed solution can be found in: + +**Reference commits:** + +```text +edgejs f87e6848 Added node test runner for wasix quickjs +edgejs 2baeceea Added node test runner wasix quickjs to gh workflow +edgejs 352c55f3 Set wasmer version to 7.2.0-alpha.3 for wasix tests +edgejs c63d7771 Install wasmer 7.2.0-alpha.3 using curl and shell script... +edgejs fbbb21f7 Updated sysroot=v2026-05-26.1 wasixcc=v0.4.3 +edgejs 566e59a5 Force llvm runtime +edgejs 9aefbbe2 Isolate mapped host directories for wasix node test runner +``` diff --git a/plans/wasix-plan/edgejs/README.md b/plans/wasix-plan/edgejs/README.md new file mode 100644 index 000000000..fa0920d17 --- /dev/null +++ b/plans/wasix-plan/edgejs/README.md @@ -0,0 +1,7 @@ +# EdgeJS Plan + +- [c-ares Request Lifecycle and Address Family Selection](01_c_ares_request_lifecycle_and_address_family_selection.md) +- [Mount Resolver and Certificate Files Instead of Hard-Coding Answers](02_mount_resolver_and_certificate_files_instead_of_hard_coding_answers.md) +- [Remove WASIX-Only Guest Workarounds After Lower-Layer Plans Land](03_remove_wasix_only_guest_workarounds_after_lower_layer_plans_land.md) +- [OpenSSL Error Mapping and Version Capability](04_openssl_error_mapping_and_version_capability.md) +- [Runner Determinism and Workflow Setup](05_runner_determinism_and_workflow_setup.md) diff --git a/plans/wasix-plan/index.md b/plans/wasix-plan/index.md new file mode 100644 index 000000000..0b77b7533 --- /dev/null +++ b/plans/wasix-plan/index.md @@ -0,0 +1,99 @@ +# WASIX QuickJS Compatibility Fix Plan + +| | | Remarks | +| --- | --- | --- | +| **Status** | ▶️ | Forward-looking plan for applying the compatibility work from a GitHub-baseline state. | +| **Severity** | High | These items explain the main gaps between a clean GitHub baseline and the local EdgeJS QuickJS WASIX Node test behavior. | + + +The detailed plan is split by project. Each project directory contains one file per problem section. + +## Baseline Assumption + +This plan is written as if the tree starts from the GitHub-style baseline: + +- Wasmer is on `main`. +- `wasix-libc` is on `main`. +- QuickJS is on `master`. +- `libuv-wasix` is on its baseline branch. +- EdgeJS is on the current WASIX QuickJS test branch. + +The commit hashes in headings are reference points only. Treat them as prior art +for the shape and scope of the change, not as statements that the baseline +already contains the behavior. + +The source comparison document is: + +```text +/Users/sadhbh/src/dev/wasmer-workspace/docs/edgejs/006_acoose_vs_dunlewy_recovered_tests.md +``` + + +## Cross-Project Rule + +Fix behavior at the lowest POSIX-compatible layer that owns it: + +```text +wasix-libc first -> Wasmer second -> libuv-wasix third -> EdgeJS/N-API/QuickJS last +``` + +EdgeJS should behave like a normal guest. If native EdgeJS works, the WASIX +version should not need EdgeJS-specific workarounds for argv, resolver files, +socket modes, stat modes, or process behavior. + + +## Project Chapters + +- [Wasmer](wasmer/README.md) +- [wasix-libc](wasix-libc/README.md) +- [libuv-wasix](libuv-wasix/README.md) +- [EdgeJS](edgejs/README.md) +- [N-API](napi/README.md) +- [QuickJS](quickjs/README.md) +- [Networking follow-up](networking/README.md) + +## Section Files + +### Wasmer + +- [UDP Datagram Receive, Readiness, and Backlog](wasmer/01_udp_datagram_receive_readiness_and_backlog.md) +- [Connected UDP Send, Peer State, and `EMSGSIZE`](wasmer/02_connected_udp_send_peer_state_and_emsgsize.md) +- [Last Socket Error / `SO_ERROR`](wasmer/03_last_socket_error_so_error.md) +- [`proc_spawn3` / `proc_exec4` for Real argv/envp](wasmer/04_proc_spawn3_proc_exec4_for_real_argv_envp.md) +- [Partial Success for `fd_write`](wasmer/05_partial_success_for_fd_write.md) +- [Future: `sendmsg` / `recvmsg` Control Data and FD Passing](wasmer/06_future_sendmsg_recvmsg_control_data_and_fd_passing.md) + +### wasix-libc + +- [Socket Type Flags: `SOCK_NONBLOCK` and `SOCK_CLOEXEC`](wasix-libc/01_socket_type_flags_sock_nonblock_and_sock_cloexec.md) +- [Boolean Socket Option Pointer Handling](wasix-libc/02_boolean_socket_option_pointer_handling.md) +- [`getsockopt(SO_ERROR)`](wasix-libc/03_getsockopt_so_error.md) +- [POSIX `st_mode` for Files and Directories](wasix-libc/04_posix_st_mode_for_files_and_directories.md) +- [Loopback Reverse Lookup](wasix-libc/05_loopback_reverse_lookup.md) + +### libuv-wasix + +- [Use POSIX Spawn on WASIX](libuv-wasix/01_use_posix_spawn_on_wasix.md) +- [UDP Disconnect and Multicast Option Compatibility](libuv-wasix/02_udp_disconnect_and_multicast_option_compatibility.md) +- [TCP Keepalive Timing Options](libuv-wasix/03_tcp_keepalive_timing_options.md) + +### EdgeJS + +- [c-ares Request Lifecycle and Address Family Selection](edgejs/01_c_ares_request_lifecycle_and_address_family_selection.md) +- [Mount Resolver and Certificate Files Instead of Hard-Coding Answers](edgejs/02_mount_resolver_and_certificate_files_instead_of_hard_coding_answers.md) +- [Remove WASIX-Only Guest Workarounds After Lower-Layer Plans Land](edgejs/03_remove_wasix_only_guest_workarounds_after_lower_layer_plans_land.md) +- [OpenSSL Error Mapping and Version Capability](edgejs/04_openssl_error_mapping_and_version_capability.md) +- [Runner Determinism and Workflow Setup](edgejs/05_runner_determinism_and_workflow_setup.md) + +### N-API + +- [QuickJS Structured Clone / Serdes for SharedArrayBuffer](napi/01_quickjs_structured_clone_serdes_for_sharedarraybuffer.md) +- [QuickJS/N-API Callsite Frames](napi/02_quickjs_napi_callsite_frames.md) + +### QuickJS + +- [WASI Stack Limit Should Match Non-WASI QuickJS](quickjs/01_wasi_stack_limit_should_match_non_wasi_quickjs.md) + +### Networking + +- [Remaining Unsatisfied Networking Plan](networking/01_remaining_unsatisfied_networking_plan.md) diff --git a/plans/wasix-plan/libuv-wasix/01_use_posix_spawn_on_wasix.md b/plans/wasix-plan/libuv-wasix/01_use_posix_spawn_on_wasix.md new file mode 100644 index 000000000..2b6f49260 --- /dev/null +++ b/plans/wasix-plan/libuv-wasix/01_use_posix_spawn_on_wasix.md @@ -0,0 +1,60 @@ +# Use POSIX Spawn on WASIX + +Why this is a problem: + +WASI/WASIX does not provide `fork()` in the POSIX sense. libuv process spawn +must use a spawn-style API, then wait for process completion through the WASIX +join mechanism. Returning success from an unimplemented fork-like path creates +fake child handles and hangs. + +When it occurs: + +- `child_process.spawn()`; +- `child_process.execFile()`; +- Node self-spawn with `process.execPath`; +- proxy tests that spawn child clients. + +Minimal manifestation: + +```js +const { spawn } = require('node:child_process'); +const child = spawn(process.execPath, ['-e', "console.log('child')"]); +child.stdout.on('data', b => process.stdout.write(b)); +``` + +Boundary: + +```text +Node child_process + -> libuv uv_spawn() + -> wasix-libc posix_spawn() + -> Wasmer proc_spawn3 + -> libuv process exit watcher via proc_join +``` + +Proposed solution: + +Implement the libuv Unix process path with `posix_spawn()` and WASIX process +join primitives. Keep argv unchanged; do not base64-wrap multiline scripts in +EdgeJS. + +Sketch: + +```c +int r = posix_spawnp(&pid, file, &file_actions, &attr, args, env); +if (r != 0) + return UV__ERR(r); + +process->pid = pid; +uv__wasix_register_process_join(loop, process); +``` + +## Proposed Solution References + +Proposed solution can be found in: + +**Reference commits:** + +```text +libuv-wasix ba06698b libuv-wasix: use WASIX posix_spawn/proc_join for child processes +``` diff --git a/plans/wasix-plan/libuv-wasix/02_udp_disconnect_and_multicast_option_compatibility.md b/plans/wasix-plan/libuv-wasix/02_udp_disconnect_and_multicast_option_compatibility.md new file mode 100644 index 000000000..4b94b24fc --- /dev/null +++ b/plans/wasix-plan/libuv-wasix/02_udp_disconnect_and_multicast_option_compatibility.md @@ -0,0 +1,53 @@ +# UDP Disconnect and Multicast Option Compatibility + +Why this is a problem: + +Unix libuv uses idioms that are not currently exposed by WASIX exactly as on +Linux, such as `connect(AF_UNSPEC)` to disconnect UDP. Some multicast options +are also not implemented by the WASIX socket layer yet. + +When it occurs: + +- dgram disconnect tests; +- multicast TTL/interface/loopback tests; +- dgram option setters that expect support or a stable no-op. + +Minimal manifestation: + +```js +const dgram = require('node:dgram'); +const s = dgram.createSocket('udp4'); +s.setMulticastTTL(16); +s.disconnect(); +``` + +Proposed solution: + +Prefer POSIX paths where WASIX supports them. Where WASIX lacks a specific +socket operation, add the smallest libuv WASIX adaptation that preserves the +libuv contract until the runtime exposes the real operation. + +Sketch: + +```c +#if defined(__wasi__) + handle->flags &= ~UV_HANDLE_UDP_CONNECTED; + return 0; +#else + return connect(fd, (const struct sockaddr*) &unspec, sizeof(unspec)); +#endif +``` + +For multicast options, return success only for options that are semantically +safe to no-op in the current package target. Otherwise return a real unsupported +error so tests do not hang behind a false success. + +## Proposed Solution References + +Proposed solution can be found in: + +**Reference commits:** + +```text +libuv-wasix 0ae770b9 multicast TTL/loop/interface shims and UDP disconnect handling +``` diff --git a/plans/wasix-plan/libuv-wasix/03_tcp_keepalive_timing_options.md b/plans/wasix-plan/libuv-wasix/03_tcp_keepalive_timing_options.md new file mode 100644 index 000000000..53caa2203 --- /dev/null +++ b/plans/wasix-plan/libuv-wasix/03_tcp_keepalive_timing_options.md @@ -0,0 +1,51 @@ +# TCP Keepalive Timing Options + +Why this is a problem: + +Node and Undici call `uv_tcp_keepalive()`. On native platforms, libuv may set +`SO_KEEPALIVE` and then tune `TCP_KEEPIDLE`, `TCP_KEEPINTVL`, and +`TCP_KEEPCNT`. WASIX may accept `SO_KEEPALIVE` but reject the TCP timing knobs. +That turns harmless keepalive setup into a failed fetch or HTTP client setup. + +Minimal manifestation: + +```js +await fetch('http://127.0.0.1:12345/'); +``` + +Boundary: + +```text +Undici fetch() + -> EdgeJS TCPWrap + -> libuv uv_tcp_keepalive() + -> setsockopt(SO_KEEPALIVE) + -> setsockopt(IPPROTO_TCP, TCP_KEEPIDLE/INTVL/KEEPCNT) +``` + +Proposed solution: + +Until WASIX supports the TCP keepalive tuning knobs, make libuv's WASIX path +accept the keepalive request without issuing unsupported timing options. The +longer-term POSIX-complete answer is to implement these socket options in +`wasix-libc` and Wasmer. + +Sketch: + +```c +#if defined(__wasi__) || defined(__wasm32__) + return 0; +#else + return uv__tcp_keepalive(fd, on, delay); +#endif +``` + +## Proposed Solution References + +Proposed solution can be found in: + +**Reference commits:** + +```text +edgejs 9fa61f18 UV keep alive fix (disable unsupported options) +``` diff --git a/plans/wasix-plan/libuv-wasix/README.md b/plans/wasix-plan/libuv-wasix/README.md new file mode 100644 index 000000000..3ba90fcd4 --- /dev/null +++ b/plans/wasix-plan/libuv-wasix/README.md @@ -0,0 +1,5 @@ +# libuv-wasix Plan + +- [Use POSIX Spawn on WASIX](01_use_posix_spawn_on_wasix.md) +- [UDP Disconnect and Multicast Option Compatibility](02_udp_disconnect_and_multicast_option_compatibility.md) +- [TCP Keepalive Timing Options](03_tcp_keepalive_timing_options.md) diff --git a/plans/wasix-plan/napi/01_quickjs_structured_clone_serdes_for_sharedarraybuffer.md b/plans/wasix-plan/napi/01_quickjs_structured_clone_serdes_for_sharedarraybuffer.md new file mode 100644 index 000000000..f6a7d230c --- /dev/null +++ b/plans/wasix-plan/napi/01_quickjs_structured_clone_serdes_for_sharedarraybuffer.md @@ -0,0 +1,82 @@ +# QuickJS Structured Clone / Serdes for SharedArrayBuffer + +Why this is a problem: + +Node worker and MessagePort internals serialize data between threads. The +QuickJS N-API serdes path must preserve `SharedArrayBuffer` backing storage and +identity semantics. Without this, worker bootstrap data can raise +`messageerror`, stall, or lose shared state. + +When it occurs: + +- worker smoke tests; +- webcrypto worker/shared-buffer paths; +- Node internals using `SharedArrayBuffer` counters. + +Minimal manifestation: + +```js +const { MessageChannel } = require('node:worker_threads'); +const { port1, port2 } = new MessageChannel(); + +port1.on('message', (value) => { + console.log(value instanceof SharedArrayBuffer, value.byteLength); +}); + +port2.postMessage(new SharedArrayBuffer(4)); +``` + +Boundary: + +```text +Node worker/message_port JS + -> N-API serializer + -> QuickJS JS_WriteObject2(JS_WRITE_OBJ_SAB | JS_WRITE_OBJ_REFERENCE) + -> N-API deserializer + -> QuickJS JS_ReadObject2(JS_READ_OBJ_SAB | JS_READ_OBJ_REFERENCE) +``` + +Proposed solution: + +Install QuickJS shared-array-buffer functions on the runtime and use +`JS_WriteObject2()` / `JS_ReadObject2()` with SAB/reference flags in the N-API +serdes implementation. + +Sketch: + +```cpp +JSSharedArrayBufferFunctions funcs{}; +funcs.sab_alloc = napi_shared_array_buffer__::alloc; +funcs.sab_free = napi_shared_array_buffer__::free; +funcs.sab_dup = napi_shared_array_buffer__::dup; +JS_SetSharedArrayBufferFunctions(rt, &funcs); + +JSValue bytes = JS_WriteObject2(ctx, obj, + JS_WRITE_OBJ_SAB | JS_WRITE_OBJ_REFERENCE); +JSValue value = JS_ReadObject2(ctx, data, len, + JS_READ_OBJ_SAB | JS_READ_OBJ_REFERENCE); +``` + +Keep allocation in an internal helper such as: + +```text +napi/quickjs/src/internal/napi_shared_array_buffer.{h,cc} +``` + +Use `new (std::nothrow)[]` / `delete[]` style consistent with the codebase. + +## Proposed Solution References + +Proposed solution can be found in: + +**Reference commits:** + +```text +napi 21f22e3 QJS: Serdes for SharedArrayBuffer +``` + +**Cross-project pointer:** + +```text +edgejs 38e01f79 Napi/Qjs: SharedArrayBuffer. Ssl certs in wasix tests. +``` diff --git a/plans/wasix-plan/napi/02_quickjs_napi_callsite_frames.md b/plans/wasix-plan/napi/02_quickjs_napi_callsite_frames.md new file mode 100644 index 000000000..bff03cee3 --- /dev/null +++ b/plans/wasix-plan/napi/02_quickjs_napi_callsite_frames.md @@ -0,0 +1,36 @@ +# QuickJS/N-API Callsite Frames + +Why this is a problem: + +Some Node internals inspect callsites to decide whether code is running inside +`node_modules`. If the QuickJS N-API path cannot expose enough callsite +information, behavior differs from V8. + +When it occurs: + +- `test-buffer-constructor-node-modules-paths`; +- stack-dependent module and error behavior. + +Minimal manifestation: + +```js +const err = new Error(); +Error.captureStackTrace(err); +console.log(err.stack); +``` + +Proposed solution: + +Expose QuickJS callsite data through the N-API callsite hooks in the same shape +expected by EdgeJS internal helpers. + +## Proposed Solution References + +Proposed solution can be found in: + +**Reference commits mentioned in the recovery notes:** + +```text +quickjs 41b00d4 Added callsites into QuickJS +napi 5a5c3b2 Improved quickjs callsites similar to v8 +``` diff --git a/plans/wasix-plan/napi/README.md b/plans/wasix-plan/napi/README.md new file mode 100644 index 000000000..900480ae0 --- /dev/null +++ b/plans/wasix-plan/napi/README.md @@ -0,0 +1,4 @@ +# N-API Plan + +- [QuickJS Structured Clone / Serdes for SharedArrayBuffer](01_quickjs_structured_clone_serdes_for_sharedarraybuffer.md) +- [QuickJS/N-API Callsite Frames](02_quickjs_napi_callsite_frames.md) diff --git a/plans/wasix-plan/networking/01_remaining_unsatisfied_networking_plan.md b/plans/wasix-plan/networking/01_remaining_unsatisfied_networking_plan.md new file mode 100644 index 000000000..5a8427a26 --- /dev/null +++ b/plans/wasix-plan/networking/01_remaining_unsatisfied_networking_plan.md @@ -0,0 +1,20 @@ +# Remaining Unsatisfied Networking Plan + +These are not part of the local-pass / GitHub-fail recovery set, but they are +the next runtime/libc networking plan items: + +- UNIX domain sockets / named pipes for HTTP/TLS pipe tests. +- `SCM_RIGHTS` descriptor passing for cluster shared handles. +- UDP multicast semantics for local and external network cases. +- UDP socket options such as receive buffer size and TTL. +- TCP keepalive options in Wasmer/`wasix-libc` rather than long-term libuv + no-ops. + +The intended ownership remains: + +```text +WITX / wasix-libc ABI if POSIX needs a new exposed syscall shape +Wasmer if the OS/runtime behavior is missing +libuv-wasix only for a temporary POSIX-facing adaptation +EdgeJS only when the behavior is genuinely guest-owned +``` diff --git a/plans/wasix-plan/networking/README.md b/plans/wasix-plan/networking/README.md new file mode 100644 index 000000000..d3eba4029 --- /dev/null +++ b/plans/wasix-plan/networking/README.md @@ -0,0 +1,3 @@ +# Networking Plan + +- [Remaining Unsatisfied Networking Plan](01_remaining_unsatisfied_networking_plan.md) diff --git a/plans/wasix-plan/quickjs/01_wasi_stack_limit_should_match_non_wasi_quickjs.md b/plans/wasix-plan/quickjs/01_wasi_stack_limit_should_match_non_wasi_quickjs.md new file mode 100644 index 000000000..46f1e8c57 --- /dev/null +++ b/plans/wasix-plan/quickjs/01_wasi_stack_limit_should_match_non_wasi_quickjs.md @@ -0,0 +1,72 @@ +# WASI Stack Limit Should Match Non-WASI QuickJS + +Why this is a problem: + +If QuickJS disables its stack limit under WASI, recursive JavaScript can run +until the Wasm stack exhausts. Then the runtime reports `RuntimeError: call +stack exhausted` instead of QuickJS producing the JS-facing overflow behavior +that Node tests expect. + +When it occurs: + +- console recursion tests; +- `test-x509-escaping`; +- `test-ttywrap-stack`; +- any test that intentionally exercises stack overflow handling. + +Minimal manifestation: + +```js +function recurse() { + return recurse(); +} + +try { + recurse(); +} catch (err) { + console.log(err.name, err.message); +} +``` + +Boundary: + +```text +JavaScript recursion + -> QuickJS stack check + -> JS RangeError/InternalError path + -> EdgeJS/Node error assertion + +Bad path: +JavaScript recursion + -> no QuickJS stack limit under WASI + -> Wasm stack exhaustion + -> Wasmer RuntimeError +``` + +Proposed solution: + +Remove the WASI-only `rt->stack_limit = 0` special case. Use the same stack +limit setup for WASI that non-WASI QuickJS uses. + +Sketch: + +```c +/* Avoid WASI-only unlimited stack behavior. */ +JS_SetMaxStackSize(rt, stack_size); +``` + +## Proposed Solution References + +Proposed solution can be found in: + +**Reference commits:** + +```text +quickjs 9a59f17 Set WASI stack limit same as non-WASI +``` + +**Related recovery note:** + +```text +quickjs cec4427 Improved stack setter +``` diff --git a/plans/wasix-plan/quickjs/README.md b/plans/wasix-plan/quickjs/README.md new file mode 100644 index 000000000..4382a7970 --- /dev/null +++ b/plans/wasix-plan/quickjs/README.md @@ -0,0 +1,3 @@ +# QuickJS Plan + +- [WASI Stack Limit Should Match Non-WASI QuickJS](01_wasi_stack_limit_should_match_non_wasi_quickjs.md) diff --git a/plans/wasix-plan/wasix-libc/01_socket_type_flags_sock_nonblock_and_sock_cloexec.md b/plans/wasix-plan/wasix-libc/01_socket_type_flags_sock_nonblock_and_sock_cloexec.md new file mode 100644 index 000000000..819638b2a --- /dev/null +++ b/plans/wasix-plan/wasix-libc/01_socket_type_flags_sock_nonblock_and_sock_cloexec.md @@ -0,0 +1,63 @@ +# Socket Type Flags: `SOCK_NONBLOCK` and `SOCK_CLOEXEC` + +Why this is a problem: + +POSIX allows callers to pass `SOCK_NONBLOCK` and `SOCK_CLOEXEC` in the socket +type. libuv does this on Unix: + +```c +socket(AF_INET, SOCK_DGRAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); +``` + +If libc forwards those bits as the socket type, the runtime may not recognize +the type as `SOCK_DGRAM` or `SOCK_STREAM`. If libc ignores the flags, the fd can +be blocking and libuv's drain loops can hang. + +When it occurs: + +- UDP tests that should get `EAGAIN`; +- HTTP/TCP sockets created through libuv; +- any socket created via the standard Unix fast path. + +Boundary: + +```text +libuv uv__socket() + -> socket(domain, type | SOCK_NONBLOCK | SOCK_CLOEXEC, protocol) + -> wasix-libc socket() + -> __wasi_sock_open(domain, base_type, protocol, &fd) + -> fcntl(fd, F_SETFL, O_NONBLOCK) +``` + +Proposed solution: + +Mask the base socket type before calling WASIX, then apply flags through the +normal fd flag paths. + +Sketch: + +```c +int socket(int domain, int type, int protocol) { + int flags = type & (SOCK_NONBLOCK | SOCK_CLOEXEC); + int base_type = type & ~(SOCK_NONBLOCK | SOCK_CLOEXEC); + + int fd = __wasi_sock_open(domain, base_type, protocol); + + if (flags & SOCK_NONBLOCK) + fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK); + if (flags & SOCK_CLOEXEC) + fcntl(fd, F_SETFD, FD_CLOEXEC); + + return fd; +} +``` + +## Proposed Solution References + +Proposed solution can be found in: + +**Reference commits:** + +```text +wasix-libc c0853db Open sockets with nonblock|cloexec flags correctly +``` diff --git a/plans/wasix-plan/wasix-libc/02_boolean_socket_option_pointer_handling.md b/plans/wasix-plan/wasix-libc/02_boolean_socket_option_pointer_handling.md new file mode 100644 index 000000000..54ecf0283 --- /dev/null +++ b/plans/wasix-plan/wasix-libc/02_boolean_socket_option_pointer_handling.md @@ -0,0 +1,58 @@ +# Boolean Socket Option Pointer Handling + +Why this is a problem: + +`setsockopt()` receives a pointer to the option value. If libc accidentally +interprets the pointer address as the option value, boolean options become +nondeterministic. A pointer address is almost always nonzero, so an intended +`0` can be treated as `1`. + +When it occurs: + +- `IPV6_V6ONLY = 0`; +- keepalive toggles; +- local-address/autoselect-family tests. + +Minimal manifestation: + +```c +int off = 0; +setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &off, sizeof(off)); +``` + +Boundary: + +```text +Node/net/libuv option setup + -> setsockopt(fd, level, name, &value, sizeof(value)) + -> wasix-libc reads pointed-to value + -> Wasmer sock_set_opt_flag/size/time +``` + +Proposed solution: + +Read the pointed-to integer/boolean value after validating `optlen`, then pass +that value through the WASIX socket option syscall. + +Sketch: + +```c +if (optlen < sizeof(int)) { + errno = EINVAL; + return -1; +} + +int enabled; +memcpy(&enabled, optval, sizeof(enabled)); +return __wasi_sock_set_opt_flag(fd, level, optname, enabled != 0); +``` + +## Proposed Solution References + +Proposed solution can be found in: + +**Reference commits:** + +```text +wasix-libc 17e686c Socket option incorrectly deferenced pointer +``` diff --git a/plans/wasix-plan/wasix-libc/03_getsockopt_so_error.md b/plans/wasix-plan/wasix-libc/03_getsockopt_so_error.md new file mode 100644 index 000000000..e93191df2 --- /dev/null +++ b/plans/wasix-plan/wasix-libc/03_getsockopt_so_error.md @@ -0,0 +1,36 @@ +# `getsockopt(SO_ERROR)` + +Why this is a problem: + +libuv uses `SO_ERROR` to turn nonblocking socket completion into the correct +JavaScript error. If libc does not expose the runtime's last socket error, +EdgeJS cannot report Node-compatible errors. + +Minimal manifestation: + +```c +int err = 0; +socklen_t len = sizeof(err); +getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &len); +``` + +Proposed solution: + +Translate `SOL_SOCKET/SO_ERROR` into the WASIX last-error socket option, return +an `int`, and keep normal `getsockopt()` pointer/length semantics. + +## Proposed Solution References + +Proposed solution can be found in: + +**Reference commits:** + +```text +wasix-libc 13626ca Last socket error +``` + +**Cross-project pair:** + +```text +wasmer 0780b11bb2a Last socket error +``` diff --git a/plans/wasix-plan/wasix-libc/04_posix_st_mode_for_files_and_directories.md b/plans/wasix-plan/wasix-libc/04_posix_st_mode_for_files_and_directories.md new file mode 100644 index 000000000..ba00d493e --- /dev/null +++ b/plans/wasix-plan/wasix-libc/04_posix_st_mode_for_files_and_directories.md @@ -0,0 +1,63 @@ +# POSIX `st_mode` for Files and Directories + +Why this is a problem: + +Node inspects POSIX `st_mode` bits to determine whether a path is a regular +file, directory, executable, or readable/writable. WASI file metadata does not +automatically look like POSIX mode bits. + +When it occurs: + +- FastUTF8 stream stat tests; +- module loader file checks; +- any Node code calling `fs.stat()`. + +Minimal manifestation: + +```js +const fs = require('node:fs'); +const st = fs.statSync(__filename); +console.log((st.mode & 0o170000).toString(8)); // should show regular-file type +``` + +Boundary: + +```text +Node fs.stat() + -> libuv uv_fs_stat() + -> wasix-libc stat() + -> WASI fd_filestat_get/path_filestat_get + -> POSIX st_mode synthesis +``` + +Proposed solution: + +Synthesize POSIX type bits and conservative permission bits in `wasix-libc`. +Do not normalize this in EdgeJS after the fact. + +Sketch: + +```c +mode_t mode = 0; + +switch (wasi_filetype) { +case __WASI_FILETYPE_DIRECTORY: + mode |= S_IFDIR | 0700; + break; +case __WASI_FILETYPE_REGULAR_FILE: + mode |= S_IFREG | 0600; + break; +} + +st->st_mode = mode; +``` + +## Proposed Solution References + +Proposed solution can be found in: + +**Reference commits:** + +```text +wasix-libc 57b1083 Fix dir and file flags +``` diff --git a/plans/wasix-plan/wasix-libc/05_loopback_reverse_lookup.md b/plans/wasix-plan/wasix-libc/05_loopback_reverse_lookup.md new file mode 100644 index 000000000..7d0e3c31b --- /dev/null +++ b/plans/wasix-plan/wasix-libc/05_loopback_reverse_lookup.md @@ -0,0 +1,54 @@ +# Loopback Reverse Lookup + +Why this is a problem: + +POSIX applications expect loopback reverse lookup and `getnameinfo()` to return +`localhost` in ordinary environments. The guest should not need EdgeJS-specific +hard-coded answers for `127.0.0.1` or `::1`. + +Minimal manifestation: + +```js +const dns = require('node:dns'); +dns.reverse('127.0.0.1', (err, names) => console.log(err, names)); +``` + +Boundary: + +```text +Node dns.reverse() + -> c-ares / getnameinfo-shaped resolver behavior + -> wasix-libc getnameinfo() + -> /etc/hosts or loopback fallback +``` + +Proposed solution: + +In libc resolver code, switch on address family and return `localhost` for IPv4 +and IPv6 loopback. Also support a mounted `/etc/hosts`; do not create synthetic +hosts files in Wasmer. + +Sketch: + +```c +switch (family) { +case AF_INET: + if (!memcmp(addr, "\x7f\x00\x00\x01", 4)) + strcpy(buf, "localhost"); + break; +case AF_INET6: + if (is_ipv6_loopback(addr, scopeid)) + strcpy(buf, "localhost"); + break; +} +``` + +## Proposed Solution References + +Proposed solution can be found in: + +**Reference commits:** + +```text +wasix-libc f157cd9 Reverse loopback +``` diff --git a/plans/wasix-plan/wasix-libc/README.md b/plans/wasix-plan/wasix-libc/README.md new file mode 100644 index 000000000..6ddfa23d1 --- /dev/null +++ b/plans/wasix-plan/wasix-libc/README.md @@ -0,0 +1,7 @@ +# wasix-libc Plan + +- [Socket Type Flags: `SOCK_NONBLOCK` and `SOCK_CLOEXEC`](01_socket_type_flags_sock_nonblock_and_sock_cloexec.md) +- [Boolean Socket Option Pointer Handling](02_boolean_socket_option_pointer_handling.md) +- [`getsockopt(SO_ERROR)`](03_getsockopt_so_error.md) +- [POSIX `st_mode` for Files and Directories](04_posix_st_mode_for_files_and_directories.md) +- [Loopback Reverse Lookup](05_loopback_reverse_lookup.md) diff --git a/plans/wasix-plan/wasmer/01_udp_datagram_receive_readiness_and_backlog.md b/plans/wasix-plan/wasmer/01_udp_datagram_receive_readiness_and_backlog.md new file mode 100644 index 000000000..01778e0d3 --- /dev/null +++ b/plans/wasix-plan/wasmer/01_udp_datagram_receive_readiness_and_backlog.md @@ -0,0 +1,116 @@ +# UDP Datagram Receive, Readiness, and Backlog + +Why this is a problem: + +UDP readiness is datagram-based, not byte-stream-based. A readiness probe must +not consume a packet that the guest later expects to receive. If `poll_oneoff()` +or the virtual socket readiness path internally calls a destructive receive, the +runtime can report `FdRead` and then make the guest's later `recvfrom()` see +`EAGAIN` or block forever. + +When it occurs: + +- zero-length UDP packets; +- recursive UDP send/receive callbacks; +- default-host dgram tests; +- any path where libuv polls first and then drains the socket. + +Minimal manifestation: + +```js +const dgram = require('node:dgram'); +const s = dgram.createSocket('udp4'); + +s.on('message', (msg) => { + console.log('received', msg.length); + s.close(); +}); + +s.bind(0, () => { + const port = s.address().port; + s.send(Buffer.alloc(0), port, '127.0.0.1'); +}); +``` + +Expected call path: + +```text +JS dgram.send() + -> libuv uv_udp_send() + -> wasix-libc sendto()/sendmsg() + -> Wasmer sock_send_to() + -> virtual-net UDP socket + +libuv poll + -> wasix-libc poll_oneoff() + -> Wasmer poll_oneoff() + -> socket readiness + -> later sock_recv_from() +``` + +Proposed solution: + +Store one complete UDP datagram in a per-socket backlog when readiness needs to +observe the socket. `poll_read_ready()` should answer from backlog first. A real +guest receive should pop from backlog; a guest peek should copy from backlog +without popping. + +Sketch: + +```rust +struct UdpBacklog { + packets: VecDeque<(Vec, SocketAddr)>, +} + +fn poll_read_ready(&mut self) -> io::Result { + if let Some((packet, _)) = self.backlog.front() { + return Ok(packet.len()); + } + + let mut tmp = vec![0; max_datagram_size()]; + match self.socket.recv_from(&mut tmp) { + Ok((n, addr)) => { + tmp.truncate(n); + self.backlog.push_back((tmp, addr)); + Ok(n) + } + Err(err) => Err(err), + } +} + +fn recv_from(&mut self, out: &mut [u8], peek: bool) -> io::Result<(usize, SocketAddr)> { + if let Some((packet, addr)) = self.backlog.front() { + let n = out.len().min(packet.len()); + out[..n].copy_from_slice(&packet[..n]); + let addr = *addr; + if !peek { + self.backlog.pop_front(); + } + return Ok((n, addr)); + } + + if peek { + self.socket.peek_from(out) + } else { + self.socket.recv_from(out) + } +} +``` + +Do this for both host and client virtual-net socket implementations. Keep +datagram boundaries intact; do not mix backlog plus a fresh receive into one +guest buffer. + +## Proposed Solution References + +Proposed solution can be found in: + +**Reference commits:** + +```text +wasmer c56897f3bec Fixed UDP socket dgram receive +wasmer 65e0eb571ef Peek UDP packets correctly +wasmer 66552d27be9 Fix merge conflict mistake +wasmer 8e92612e917 Merge branch 'main' into udp-datagram-receive-and-last-err +wasmer bee1e0ab8ae Merge branch 'udp-datagram-receive-and-last-err' into tmp-work-4 +``` diff --git a/plans/wasix-plan/wasmer/02_connected_udp_send_peer_state_and_emsgsize.md b/plans/wasix-plan/wasmer/02_connected_udp_send_peer_state_and_emsgsize.md new file mode 100644 index 000000000..d649f07da --- /dev/null +++ b/plans/wasix-plan/wasmer/02_connected_udp_send_peer_state_and_emsgsize.md @@ -0,0 +1,74 @@ +# Connected UDP Send, Peer State, and `EMSGSIZE` + +Why this is a problem: + +Connected UDP is still UDP. A vectored send represents one datagram, not one +datagram per iovec. The runtime also needs to remember the peer chosen by +`connect()` so `getpeername()` and connected sends behave normally. + +When it occurs: + +- `test-dgram-connect-send-multi-string-array`; +- `test-dgram-connect-send-multi-buffer-copy`; +- `test-dgram-msgsize`; +- c-ares connected UDP resolver paths. + +Minimal manifestation: + +```js +const dgram = require('node:dgram'); +const s = dgram.createSocket('udp4'); + +s.bind(0, () => { + s.connect(s.address().port, '127.0.0.1', () => { + s.send(['he', 'llo']); // one UDP datagram: "hello" + }); +}); +``` + +Callgraph and boundary: + +```text +Node dgram array send + -> libuv uv_udp_send() + -> wasix-libc sendmsg()/send() + -> Wasmer connected UDP send + -> virtual-net send_to(peer, coalesced_iovs) +``` + +Proposed solution: + +- During UDP `connect()`, preserve the upgraded/auto-bound socket. +- Store the connected peer in socket state. +- For connected UDP vectored send, concatenate iovecs into a single datagram. +- Map oversized datagrams to `Errno::Msgsize`, not a generic I/O error. + +Sketch: + +```rust +fn udp_connected_send(iovs: &[IoSlice<'_>], peer: SocketAddr) -> Result { + let len: usize = iovs.iter().map(|iov| iov.len()).sum(); + if len > max_udp_payload() { + return Err(Errno::Msgsize); + } + + let mut packet = Vec::with_capacity(len); + for iov in iovs { + packet.extend_from_slice(iov); + } + + socket.send_to(&packet, peer).map_err(map_socket_err)?; + Ok(len) +} +``` + +## Proposed Solution References + +Proposed solution can be found in: + +**Reference commits:** + +```text +wasmer 50e5c1f1375 connected UDP send() now coalesces iovecs into one datagram... +wasmer dc9e005159a WASIX UDP connect() now preserves the auto-bound UDP socket... +``` diff --git a/plans/wasix-plan/wasmer/03_last_socket_error_so_error.md b/plans/wasix-plan/wasmer/03_last_socket_error_so_error.md new file mode 100644 index 000000000..5cb300c31 --- /dev/null +++ b/plans/wasix-plan/wasmer/03_last_socket_error_so_error.md @@ -0,0 +1,69 @@ +# Last Socket Error / `SO_ERROR` + +Why this is a problem: + +Node and libuv check `getsockopt(SO_ERROR)` after nonblocking connect and write +paths. If Wasmer loses the actual socket error, JavaScript sees the wrong error, +for example `EPIPE` or `ECONNRESET` where the test expects `ECONNREFUSED`. + +When it occurs: + +- refused HTTP/TLS connects; +- proxy connection-refused tests; +- tests asserting exact Node error codes. + +Minimal manifestation: + +```js +const net = require('node:net'); + +net.connect({ host: '127.0.0.1', port: 9 }) + .on('error', (err) => console.log(err.code)); +``` + +Boundary: + +```text +libuv uv__stream_connect() + -> getsockopt(fd, SOL_SOCKET, SO_ERROR, ...) + -> wasix-libc __wasi_sock_get_opt_*() + -> Wasmer socket last_error +``` + +Proposed solution: + +Record the last connect/send/recv error on the Wasmer socket object and expose +it through the WASIX socket-option path. Reading `SO_ERROR` should return and, +where POSIX requires it, clear the pending error. + +Sketch: + +```rust +struct WasiSocket { + last_error: Option, +} + +fn set_last_socket_error(&mut self, err: Errno) { + self.last_error = Some(err); +} + +fn get_so_error(&mut self) -> Errno { + self.last_error.take().unwrap_or(Errno::Success) +} +``` + +## Proposed Solution References + +Proposed solution can be found in: + +**Reference commits:** + +```text +wasmer 0780b11bb2a Last socket error +``` + +**Cross-project pair:** + +```text +wasix-libc 13626ca Last socket error +``` diff --git a/plans/wasix-plan/wasmer/04_proc_spawn3_proc_exec4_for_real_argv_envp.md b/plans/wasix-plan/wasmer/04_proc_spawn3_proc_exec4_for_real_argv_envp.md new file mode 100644 index 000000000..a225e81e4 --- /dev/null +++ b/plans/wasix-plan/wasmer/04_proc_spawn3_proc_exec4_for_real_argv_envp.md @@ -0,0 +1,83 @@ +# `proc_spawn3` / `proc_exec4` for Real argv/envp + +Why this is a problem: + +Older WASIX process syscalls encode `argv` and `envp` as newline-delimited +strings. That cannot represent an argument that itself contains `\n` or `\r`. +Node frequently spawns itself as: + +```text +edge -e "" +``` + +Splitting that by line feed turns one argument into many. + +Minimal manifestation: + +```js +const { spawnSync } = require('node:child_process'); + +const script = [ + "console.log('line 1')", + "console.log('line 2')", +].join('\n'); + +const r = spawnSync(process.execPath, ['-e', script], { encoding: 'utf8' }); +console.log(r.stdout); +``` + +Boundary: + +```text +Node child_process.spawn() + -> libuv uv_spawn() + -> wasix-libc posix_spawn() + -> wasix_32v1.proc_spawn3(argv_ptrs, argv_lens, env_ptrs, env_lens) + -> Wasmer process runner +``` + +Proposed solution: + +Add Wasmer imports that receive argument vectors as pointer/count arrays rather +than delimiter-encoded strings. The runtime should reconstruct exact byte +strings per argument. + +Sketch: + +```witx +(@interface func (export "proc_spawn3") + (param $path string) + (param $argv_ptrs (@witx pointer u32)) + (param $argv_lens (@witx pointer size)) + (param $argv_len size) + ... + (result $errno errno)) +``` + +Runtime sketch: + +```rust +let argv = read_string_array(memory, argv_ptrs, argv_lens, argv_len)?; +let envp = read_string_array(memory, env_ptrs, env_lens, env_len)?; +spawn_process(path, argv, envp, file_actions) +``` + +## Proposed Solution References + +Proposed solution can be found in: + +**Reference commits:** + +```text +wasmer d80c3d8c980 Add proc_spawn3 and proc_exec4... +wasmer b5880b5268b apply review comments +wasmer e9d1478b914 fix tests +wasmer bda2ead2504 Merge branch 'feat/wasix-proc-spawn3' into tmp-work-4 +``` + +**Cross-project pair:** + +```text +wasix-libc proc_spawn3/proc_exec4 lowering +libuv-wasix ba06698b libuv-wasix: use WASIX posix_spawn/proc_join... +``` diff --git a/plans/wasix-plan/wasmer/05_partial_success_for_fd_write.md b/plans/wasix-plan/wasmer/05_partial_success_for_fd_write.md new file mode 100644 index 000000000..61bfe78d0 --- /dev/null +++ b/plans/wasix-plan/wasmer/05_partial_success_for_fd_write.md @@ -0,0 +1,53 @@ +# Partial Success for `fd_write` + +Why this is a problem: + +POSIX-like write APIs can report partial success. If the first iovec writes and +a later iovec fails, returning total failure loses bytes already accepted by the +kernel/runtime and can confuse stream accounting. + +When it occurs: + +- process stdout/stderr pipes; +- stream and HTTP output accounting; +- tests around bytes written and close/error ordering. + +Minimal manifestation: + +```c +struct iovec iov[2] = { + { .iov_base = "ok", .iov_len = 2 }, + { .iov_base = bad_ptr, .iov_len = 4 }, +}; + +writev(fd, iov, 2); // should be allowed to return 2 +``` + +Proposed solution: + +If one or more iovecs have already written successfully, return the successful +byte count instead of converting the later error into total failure. + +Sketch: + +```rust +let mut written = 0; +for iov in iovs { + match write_one(iov) { + Ok(n) => written += n, + Err(err) if written > 0 => return Ok(written), + Err(err) => return Err(err), + } +} +Ok(written) +``` + +## Proposed Solution References + +Proposed solution can be found in: + +**Reference commits:** + +```text +wasmer 945aac9b18f fd_write should not fail it there was at least one success +``` diff --git a/plans/wasix-plan/wasmer/06_future_sendmsg_recvmsg_control_data_and_fd_passing.md b/plans/wasix-plan/wasmer/06_future_sendmsg_recvmsg_control_data_and_fd_passing.md new file mode 100644 index 000000000..e11e19559 --- /dev/null +++ b/plans/wasix-plan/wasmer/06_future_sendmsg_recvmsg_control_data_and_fd_passing.md @@ -0,0 +1,62 @@ +# Future: `sendmsg` / `recvmsg` Control Data and FD Passing + +Why this is a problem: + +Plain IPC messages can travel as stream bytes. Passing a socket or server handle +between processes requires ancillary data, normally `sendmsg()` with +`SCM_RIGHTS`. Node cluster shared UDP handles need this. + +When it occurs: + +- `test-dgram-cluster-*`; +- `test-dgram-bind-shared-ports`; +- cluster rows that need a worker to receive a shared handle. + +Minimal manifestation: + +```js +// Parent +worker.send({ cmd: 'use this socket' }, udpSocket); + +// Child +process.on('message', (msg, handle) => { + handle.on('message', () => {}); +}); +``` + +Boundary: + +```text +Node cluster/fork + -> child_process IPC channel on fd 3 + -> libuv uv_write2() + -> sendmsg(..., SCM_RIGHTS) + -> wasix-libc sendmsg() + -> Wasmer sock_send_msg(control) + -> receiver recvmsg(control) +``` + +Proposed solution: + +Implement runtime-owned handle passing: + +- parse WASIX control-message buffers; +- validate `SOL_SOCKET` + `SCM_RIGHTS`; +- duplicate sender fd entries with rights/cloexec metadata; +- queue control metadata with socketpair stream data; +- serialize received handles into the receiver fd table during `sock_recv_msg`; +- report truncation if the receiver's control buffer is too small. + +Do not silently drop control data. Until implemented, returning `ENOTSUP` is +more honest than pretending descriptor passing worked. + +## Proposed Solution References + +Proposed solution can be found in: + +**Reference status:** + +```text +WITX/libc payload ABI exists as plan/input. +Phase 2 SCM_RIGHTS/handle passing remains a Wasmer runtime task. +``` diff --git a/plans/wasix-plan/wasmer/README.md b/plans/wasix-plan/wasmer/README.md new file mode 100644 index 000000000..82eff47e1 --- /dev/null +++ b/plans/wasix-plan/wasmer/README.md @@ -0,0 +1,8 @@ +# Wasmer Plan + +- [UDP Datagram Receive, Readiness, and Backlog](01_udp_datagram_receive_readiness_and_backlog.md) +- [Connected UDP Send, Peer State, and `EMSGSIZE`](02_connected_udp_send_peer_state_and_emsgsize.md) +- [Last Socket Error / `SO_ERROR`](03_last_socket_error_so_error.md) +- [`proc_spawn3` / `proc_exec4` for Real argv/envp](04_proc_spawn3_proc_exec4_for_real_argv_envp.md) +- [Partial Success for `fd_write`](05_partial_success_for_fd_write.md) +- [Future: `sendmsg` / `recvmsg` Control Data and FD Passing](06_future_sendmsg_recvmsg_control_data_and_fd_passing.md) From 2d9beb2fed97aaf22ac4a88146469669629517ab Mon Sep 17 00:00:00 2001 From: Sadhbh Code Date: Mon, 15 Jun 2026 10:43:27 +0100 Subject: [PATCH 24/65] wasix plan update --- ..._lifecycle_and_address_family_selection.md | 151 +++++++++++----- ...te_files_instead_of_hard_coding_answers.md | 129 ++++++++++---- ...orkarounds_after_lower_layer_plans_land.md | 135 ++++++++++++--- ...sl_error_mapping_and_version_capability.md | 117 +++++++++---- ...5_runner_determinism_and_workflow_setup.md | 123 +++++++++---- plans/wasix-plan/index.md | 2 +- .../01_use_posix_spawn_on_wasix.md | 101 ++++++----- ...nect_and_multicast_option_compatibility.md | 108 ++++++++---- .../03_tcp_keepalive_timing_options.md | 87 ++++++---- ...ured_clone_serdes_for_sharedarraybuffer.md | 110 ++++++------ .../napi/02_quickjs_napi_callsite_frames.md | 93 +++++++--- ...ack_limit_should_match_non_wasi_quickjs.md | 91 +++++----- ...pe_flags_sock_nonblock_and_sock_cloexec.md | 70 ++++++-- ..._boolean_socket_option_pointer_handling.md | 55 ++++-- .../wasix-libc/03_getsockopt_so_error.md | 54 +++++- ...posix_st_mode_for_files_and_directories.md | 83 +++++++-- .../wasix-libc/05_loopback_reverse_lookup.md | 79 +++++++-- ..._datagram_receive_readiness_and_backlog.md | 144 ++++++++++++---- ...nected_udp_send_peer_state_and_emsgsize.md | 161 +++++++++++++++--- .../wasmer/03_last_socket_error_so_error.md | 114 +++++++++++-- ...oc_spawn3_proc_exec4_for_real_argv_envp.md | 118 ++++++++++--- .../wasmer/05_partial_success_for_fd_write.md | 73 +++++++- ...msg_recvmsg_control_data_and_fd_passing.md | 134 ++++++++++++--- 23 files changed, 1778 insertions(+), 554 deletions(-) diff --git a/plans/wasix-plan/edgejs/01_c_ares_request_lifecycle_and_address_family_selection.md b/plans/wasix-plan/edgejs/01_c_ares_request_lifecycle_and_address_family_selection.md index dd00f3559..548b14128 100644 --- a/plans/wasix-plan/edgejs/01_c_ares_request_lifecycle_and_address_family_selection.md +++ b/plans/wasix-plan/edgejs/01_c_ares_request_lifecycle_and_address_family_selection.md @@ -1,66 +1,131 @@ -# c-ares Request Lifecycle and Address Family Selection +# EdgeJS: c-ares request lifecycle and address-family selection Why this is a problem: -The c-ares binding owns request lifecycle. If a resolver channel is cancelled, -still-active requests must complete with cancellation rather than vanishing. -When callers request IPv4 or IPv6, EdgeJS must pass that family through -consistently. - -When it occurs: - -- `test-dns-channel-cancel`; -- `test-dns-channel-cancel-promise`; -- `test-dns-cancel-reverse-lookup`; -- `test-dns-multi-channel`; -- `test-http-autoselectfamily`. - -Minimal manifestation: +EdgeJS owns the JavaScript-facing DNS binding around c-ares. If a resolver +channel is cancelled or destroyed while requests are still active, Node expects +those requests to complete with a cancellation error. If EdgeJS only tears down +the c-ares channel, the JavaScript callback can be lost and the test waits until +its timeout. Address-family selection has the same shape: JavaScript asks for +IPv4 or IPv6, but if the binding does not propagate that choice into c-ares, the +wrong family can be returned and downstream UDP/TCP tests start failing for the +wrong reason. + +This occurs in DNS tests that cancel c-ares work, use several resolver channels, +perform reverse lookups, or ask for a specific address family. + +Why native EdgeJS passes, and why WASIX still needs this: + +Native EdgeJS usually runs on top of mature OS networking, native c-ares timing, +and the V8/Node execution profile that the binding was first exercised against. +Those conditions can make cancellation races and address-family mistakes much +harder to see: DNS work completes quickly, channel teardown ordering differs, and +IPv4/IPv6 answers often come from the host resolver in an order that happens to +match the test. Under WASIX, c-ares is running through libuv, `wasix-libc`, and +Wasmer socket/poll behavior, so cancellation and resolver completion ordering is +more deterministic and exposed the missing EdgeJS-owned request bookkeeping. The +needed EdgeJS change is not a WASIX socket workaround; it is making the c-ares +binding obey the Node contract even when the WASIX runtime schedules DNS work +differently from native. + +Minimal Example: ```js +const assert = require('node:assert'); const dns = require('node:dns'); -const r = new dns.Resolver(); -r.resolve4('example.com', (err) => console.log(err && err.code)); -r.cancel(); // callback must still run with ECANCELLED +const resolver = new dns.Resolver(); +let called = false; +resolver.resolve4('example.invalid', (err) => { + called = true; + assert(err); + assert.strictEqual(err.code, 'ECANCELLED'); +}); +resolver.cancel(); + +setTimeout(() => { + assert.strictEqual(called, true); +}, 10); + +dns.lookup('localhost', { family: 4 }, (err, address, family) => { + assert.ifError(err); + assert.strictEqual(family, 4); +}); ``` -Boundary: +Failing tests that showed this class: ```text -Node dns.Resolver - -> EdgeJS c-ares binding - -> c-ares channel/request objects - -> UDP sockets via libuv/wasix-libc/Wasmer +parallel/test-c-ares +parallel/test-dns-cancel-reverse-lookup +parallel/test-dns-channel-cancel +parallel/test-dns-channel-cancel-promise +parallel/test-dns-multi-channel +parallel/test-dns-perf_hooks +parallel/test-dns-resolveany +parallel/test-dns-resolver-max-timeout +parallel/test-dns-setserver-when-querying +parallel/test-dns ``` -Proposed solution: +Callgraph and boundary: -Track active requests by c-ares channel. On cancellation, complete pending -requests with `ECANCELLED`. Respect the family requested by JavaScript when -building `GetAddrInfo` hints. +Current problematic path: -Sketch: +```text +JavaScript dns.Resolver.resolve4() + -> EdgeJS src/edge_cares_wrap.cc creates a request wrapper + -> c-ares owns the in-flight query + +JavaScript resolver.cancel() + -> EdgeJS cancels/destroys c-ares channel + HERE IS THE PROBLEM: active EdgeJS request wrappers may not all be completed + with ECANCELLED, so JavaScript mustCall callbacks are never reached. + +JavaScript dns.lookup(..., { family: 4 }) + -> EdgeJS GetAddrInfo/GetNameInfo wrapper + HERE IS THE PROBLEM: requested family can be lost or loosened before the + c-ares query, so IPv6 can leak into IPv4-only paths or the reverse. +``` -```cpp -struct ChannelState { - std::unordered_set active; -}; +The boundary is EdgeJS <-> c-ares. This is not a Wasmer socket fix: the runtime +can deliver UDP packets correctly and the binding can still lose request +completion state. -void Cancel(ChannelState* channel) { - for (Request* req : channel->active) - req->Complete(ARES_ECANCELLED); - channel->active.clear(); -} -``` +Proposed solution: -## Proposed Solution References +Keep this fix in EdgeJS because the binding owns c-ares request lifetime and the +Node-visible DNS callback contract. + +Relevant EdgeJS code paths: -Proposed solution can be found in: +```text +~/src/edgejs/src/edge_cares_wrap.cc +~/src/edgejs/test/parallel/test-dns-*.js +~/src/edgejs/test/parallel/test-c-ares.js +``` -**Reference commits:** +Proposed callgraph: ```text -edgejs 929a9151 c-ares requests are now tracked per channel... -edgejs 64faa6ca Respect address family (IPv4 or IPv6) when asked GetAddrInfo +JavaScript resolver.cancel() + -> EdgeJS channel cancel + -> EdgeJS iterates the channel's active request set + -> each still-active request completes exactly once with ECANCELLED + -> request is removed from the active set + -> JavaScript callback/promise observes the expected cancellation + +JavaScript dns.lookup(..., { family }) + -> EdgeJS parses requested family + -> EdgeJS passes matching c-ares hints/query type + -> JavaScript receives an address with the requested family ``` + +The minimal implementation is to make each c-ares channel own an explicit set of +active EdgeJS request objects, complete still-active requests during cancellation, +and preserve the requested address family when constructing c-ares work. + +## Proposed Solution References + +- edgejs `929a9151` `c-ares requests are now tracked per channel, cancellation completes still-active requests with ECANCELLED, reverse lookup includes h_name, and loopback reverse/nameinfo returns localhost` +- edgejs `64faa6ca` `Respect address family (IPv4 or IPv6) when asked GetAddrInfo` diff --git a/plans/wasix-plan/edgejs/02_mount_resolver_and_certificate_files_instead_of_hard_coding_answers.md b/plans/wasix-plan/edgejs/02_mount_resolver_and_certificate_files_instead_of_hard_coding_answers.md index 14c28da5b..84aeddd4b 100644 --- a/plans/wasix-plan/edgejs/02_mount_resolver_and_certificate_files_instead_of_hard_coding_answers.md +++ b/plans/wasix-plan/edgejs/02_mount_resolver_and_certificate_files_instead_of_hard_coding_answers.md @@ -1,54 +1,119 @@ -# Mount Resolver and Certificate Files Instead of Hard-Coding Answers +# EdgeJS: mount resolver and certificate files instead of hard-coding answers Why this is a problem: -The guest needs normal files such as `/etc/hosts` and SSL certs. Hard-coding -loopback answers in EdgeJS or synthesizing files in Wasmer hides a missing -package/runtime configuration problem. +A POSIX-like guest expects resolver configuration and certificate material to +come from the filesystem. If the WASIX package does not expose `/etc/hosts` or +the SSL certificate directory, EdgeJS may be tempted to hard-code `localhost`, +loopback reverse lookups, or certificate search paths. That makes EdgeJS tests +pass for the wrong reason and leaves other WASIX programs broken. -When it occurs: +This occurs when DNS tests resolve `localhost` or TLS/HTTPS tests need the same +certificate fixtures that native Node sees. -- DNS loopback and `localhost` tests; -- TLS tests needing CA/cert fixtures; -- package execution in GitHub where host paths differ from local paths. +Why native EdgeJS passes, and why WASIX still needs this: -Minimal manifestation: +Native EdgeJS inherits the host operating system's `/etc/hosts`, resolver +configuration, and certificate locations. The same JavaScript therefore sees a +normal `localhost` answer and OpenSSL can find the host's certificate setup or +the test fixture paths. In WASIX, the guest sees only what the package and runner +mount into its filesystem. If `/etc/hosts` or `/usr/local/ssl` is absent, EdgeJS +looks broken even though the native binary passes. The right EdgeJS-side change +is test/package configuration: mount the same kind of files a POSIX process would +have, instead of adding hard-coded resolver or TLS answers inside EdgeJS or +Wasmer. + +Minimal Example: ```js -require('node:dns').lookup('localhost', console.log); -require('node:https').get('https://localhost:8443/', console.log); +const assert = require('node:assert'); +const dns = require('node:dns'); +const https = require('node:https'); + +dns.lookup('localhost', (err, address) => { + assert.ifError(err); + assert(address === '127.0.0.1' || address === '::1'); +}); + +https.get('https://localhost:443', (res) => { + res.resume(); +}).on('error', (err) => { + // The test may expect a TLS/socket error, but not a missing cert-store or + // missing resolver-file setup error. + assert.notStrictEqual(err.code, 'ENOENT'); +}); ``` -Proposed solution: +Failing tests that showed this class: + +```text +parallel/test-dns +parallel/test-dns-resolveany +parallel/test-http-autoselectfamily +parallel/test-https-autoselectfamily +sequential/test-https-connect-localport +parallel/test-tls-env-extra-ca +parallel/test-tls-env-bad-extra-ca +parallel/test-tls-env-extra-ca-with-options +``` -Add real package files and mount them with `wasmer.toml` or the test runner: +Callgraph and boundary: -```toml -[[command]] -name = "edge" -module = "edgejs" +Current problematic path: -[[command.volumes]] -source = "./etc" -target = "/etc" +```text +JavaScript dns.lookup('localhost') + -> EdgeJS DNS binding + -> wasix-libc resolver opens /etc/hosts + HERE IS THE PROBLEM: package/runner may not mount /etc/hosts, so resolver + behavior depends on hard-coded EdgeJS answers or fails differently from POSIX. + +JavaScript TLS/HTTPS test + -> EdgeJS crypto/tls binding + -> OpenSSL default certificate lookup + HERE IS THE PROBLEM: certificate fixture directory is not visible at the + guest path OpenSSL expects. ``` -Runner sketch: +The boundary is package/runner configuration <-> guest filesystem. Wasmer should +not synthesize project-specific files globally, and EdgeJS should not hard-code +resolver answers that belong in resolver files. -```sh -wasmer run --net \ - --volume "$EDGEJS_ROOT/quickjs-wasm/etc:/etc" \ - --volume "$EDGEJS_ROOT/ssl-certs:/usr/local/ssl" \ - "$WASIX_EDGEJS_PACKAGE_DIR" -- "$test" -``` +Proposed solution: -## Proposed Solution References +Mount the files through the EdgeJS WASIX package and runner. Keep the resolver +and TLS configuration ordinary from the guest's point of view. -Proposed solution can be found in: +Relevant EdgeJS code paths: -**Reference commits:** +```text +~/src/edgejs/quickjs-wasm/wasmer.toml +~/src/edgejs/quickjs-wasm/etc/hosts +~/src/edgejs/ssl-certs +~/src/edgejs/scripts/edge-wasix-node-runner.sh +``` + +Proposed callgraph: ```text -edgejs dc9a0465 Added /etc/hosts, removed stream type normalization -edgejs 38e01f79 Napi/Qjs: SharedArrayBuffer. Ssl certs in wasix tests. +wasmer run quickjs-wasm + -> package mounts quickjs-wasm/etc at /etc + -> package/runner mounts ssl-certs at /usr/local/ssl + +JavaScript dns.lookup('localhost') + -> wasix-libc resolver reads /etc/hosts + -> normal POSIX-style answer is returned + +JavaScript TLS/HTTPS test + -> OpenSSL resolves cert paths inside /usr/local/ssl + -> test observes real TLS/socket semantics instead of missing-file setup noise ``` + +This keeps the compatibility rule intact: configuration files are mounted like a +normal system image, not emulated inside Wasmer and not patched into EdgeJS DNS +logic. + +## Proposed Solution References + +- edgejs `dc9a0465` `Added /etc/hosts, removed stream type normalization` +- edgejs `38e01f79` `Napi/Qjs: SharedArrayBuffer. Ssl certs in wasix tests.` diff --git a/plans/wasix-plan/edgejs/03_remove_wasix_only_guest_workarounds_after_lower_layer_plans_land.md b/plans/wasix-plan/edgejs/03_remove_wasix_only_guest_workarounds_after_lower_layer_plans_land.md index cbadef3a3..22663a6e0 100644 --- a/plans/wasix-plan/edgejs/03_remove_wasix_only_guest_workarounds_after_lower_layer_plans_land.md +++ b/plans/wasix-plan/edgejs/03_remove_wasix_only_guest_workarounds_after_lower_layer_plans_land.md @@ -1,44 +1,127 @@ -# Remove WASIX-Only Guest Workarounds After Lower-Layer Plans Land +# EdgeJS: remove WASIX-only guest workarounds after lower-layer plans land Why this is a problem: -Workarounds inside EdgeJS can make the guest pass one test while hiding a broken -WASIX or libc contract. For example, base64-wrapping `edge -e` scripts in -EdgeJS avoids newline-splitting but leaves every other WASIX program with broken -argv. +EdgeJS is the guest. If native EdgeJS works through POSIX APIs, the WASIX build +should work through the same APIs once `wasix-libc` and Wasmer provide correct +semantics. WASIX-only rewrites in EdgeJS hide missing platform behavior and make +EdgeJS diverge from native Node behavior. -When it occurs: +This occurs when EdgeJS rewrites multiline `-e` arguments, hard-codes loopback +resolver answers, normalizes stat modes, or changes stream type behavior only +under `__wasi__`. -- multiline `-e` spawn; -- loopback resolver shortcuts; -- manual stat mode normalization; -- stream type normalization. +Why native EdgeJS passes, and why WASIX still needs this: -Minimal manifestation: +Native EdgeJS passes because the host libc/kernel already preserve argv strings, +provide resolver files, return conventional stat modes, and implement the socket +semantics that libuv expects. WASIX failures in this bucket came from those lower +layers being incomplete, not from JavaScript wanting different behavior. EdgeJS +workarounds were useful to prove causality, but they are the wrong final shape: +they make this one guest special while every other POSIX-style WASIX program +still sees the broken behavior. The EdgeJS action here is therefore mostly +negative: remove guest-only branches once `wasix-libc`, Wasmer, or libuv-wasix +implements the native contract. -```cpp -// Avoid this as final design: -if (host == "127.0.0.1") - return "localhost"; +Minimal Example: -// Avoid this as final design: -script = "eval(Buffer.from('" + Base64Encode(script) + "', 'base64').toString())"; +```js +const assert = require('node:assert'); +const { spawnSync } = require('node:child_process'); +const dns = require('node:dns'); +const fs = require('node:fs'); + +const script = "console.log('a')\nconsole.log('b')"; +const child = spawnSync(process.execPath, ['-e', script], { encoding: 'utf8' }); +assert.strictEqual(child.status, 0); +assert.match(child.stdout, /a\nb/); + +dns.lookupService('127.0.0.1', 80, (err, host) => { + assert.ifError(err); + assert.strictEqual(host, 'localhost'); +}); + +const mode = fs.statSync(__filename).mode; +assert.notStrictEqual(mode & 0o777, 0); +``` + +Failing tests that showed this class before lower-layer fixes: + +```text +client-proxy/test-http-proxy-fetch +parallel/test-stream-readable-unpipe-resume +parallel/test-fastutf8stream-mode +parallel/test-http-client-default-headers-exist +parallel/test-dns-* ``` +Callgraph and boundary: + +Current problematic path: + +```text +JavaScript child_process.spawn(process.execPath, ['-e', multilineScript]) + -> EdgeJS process wrapper rewrites script text for WASIX + HERE IS THE PROBLEM: EdgeJS is compensating for old newline-delimited + WASIX argv instead of using a libc/runtime argv API that preserves strings. + +JavaScript dns.lookupService('127.0.0.1') + -> EdgeJS DNS binding special-cases loopback + HERE IS THE PROBLEM: resolver behavior is hard-coded in the guest instead + of coming from wasix-libc resolver semantics and mounted /etc/hosts. + +JavaScript fs.statSync(path) + -> EdgeJS normalizes mode bits + HERE IS THE PROBLEM: stat mode compatibility belongs in wasix-libc/Wasmer. +``` + +The boundary is EdgeJS <-> POSIX-facing libc/runtime APIs. Any workaround here +should be treated as temporary investigation scaffolding. + Proposed solution: -Keep temporary workarounds only while proving a cause. Once `wasix-libc` or -Wasmer owns the missing behavior, remove EdgeJS special cases and let EdgeJS use -normal POSIX-facing APIs. +Remove EdgeJS-only compatibility shims once the lower layers provide the needed +behavior: -## Proposed Solution References +- argv/envp preservation belongs in `wasix-libc` and Wasmer process syscalls; +- resolver behavior belongs in `wasix-libc` plus mounted resolver files; +- file mode behavior belongs in `wasix-libc`/Wasmer stat translation; +- stream/socket behavior belongs in libuv, `wasix-libc`, and Wasmer. -Proposed solution can be found in: +Relevant EdgeJS code paths: + +```text +~/src/edgejs/src/edge_process_wrap.cc +~/src/edgejs/src/edge_spawn_sync.cc +~/src/edgejs/src/edge_cares_wrap.cc +~/src/edgejs/scripts/edge-wasix-node-runner.sh +``` -**Reference commits:** +Proposed callgraph: ```text -edgejs f1999f45 Removed hacks: loopback, and spawn -edgejs 27118329 Fixes around edge process wrap, plus w/a for edge -e eval -edgejs 61ba9604 various fixes +JavaScript child_process.spawn(process.execPath, ['-e', multilineScript]) + -> EdgeJS forwards argv unchanged + -> libuv-wasix posix_spawnp() + -> wasix-libc proc_spawn3/proc_exec4 argv pointer array + -> Wasmer starts child with exact argv strings + +JavaScript dns.lookupService(loopback) + -> EdgeJS calls normal resolver path + -> wasix-libc reads /etc/hosts / resolver config + -> normal POSIX-style answer + +JavaScript fs.statSync(path) + -> EdgeJS exposes stat result unchanged + -> wasix-libc/Wasmer provide compatible mode bits ``` + +The rule is simple: if a JS test passes natively and only fails on WASIX because +of libc/runtime semantics, prefer fixing libc/runtime and deleting the EdgeJS +branch. + +## Proposed Solution References + +- edgejs `f1999f45` `Removed hacks: loopback, and spawn` +- edgejs `27118329` `Fixes around edge process wrap, plus w/a for edge -e eval` +- edgejs `61ba9604` `various fixes` diff --git a/plans/wasix-plan/edgejs/04_openssl_error_mapping_and_version_capability.md b/plans/wasix-plan/edgejs/04_openssl_error_mapping_and_version_capability.md index e9a1fb89b..bc6cd871f 100644 --- a/plans/wasix-plan/edgejs/04_openssl_error_mapping_and_version_capability.md +++ b/plans/wasix-plan/edgejs/04_openssl_error_mapping_and_version_capability.md @@ -1,56 +1,109 @@ -# OpenSSL Error Mapping and Version Capability +# EdgeJS: OpenSSL error mapping and version capability Why this is a problem: -Node tests often assert specific OpenSSL-backed error classes and messages. If -the binding maps the wrong item from the OpenSSL error stack, JavaScript gets a -different code from native Node. Some crypto/PQC tests also require OpenSSL 3.5 -capability. +Node crypto and TLS tests assert exact error classes, OpenSSL reasons, and +feature availability. If EdgeJS uses an older OpenSSL build, version-gated +algorithms are unavailable. If the binding maps the wrong OpenSSL error from the +error stack, JavaScript receives a misleading Node error even when the lower +socket/TLS behavior is correct. -When it occurs: +This occurs in crypto key generation, PQC/WebCrypto, TLS parser, and TLS +handshake tests that inspect the exact error code or expected algorithm support. -- TLS parser/error tests; -- crypto key generation tests; -- PQC algorithm tests. +Why native EdgeJS passes, and why WASIX still needs this: -Minimal manifestation: +Native EdgeJS was built and tested against the native OpenSSL dependency and the +error-stack behavior used by that build. The WASIX build can lag behind in two +ways: it may compile a different OpenSSL version, and the WASIX/QuickJS path can +exercise slightly different failure ordering around TLS/socket errors. If EdgeJS +selects the wrong OpenSSL stack entry, native may still pass because the stack is +shaped differently there, while WASIX exposes the wrong Node-visible code. The +valid EdgeJS work is to make the OpenSSL dependency and error mapping match the +Node contract for all builds; lower layers still own socket `SO_ERROR`, but +EdgeJS owns translating OpenSSL reasons into JavaScript errors. + +Minimal Example: ```js +const assert = require('node:assert'); +const crypto = require('node:crypto'); const tls = require('node:tls'); -const s = tls.connect({ port: 1 }); -s.on('error', (err) => console.log(err.code, err.message)); + +assert.doesNotThrow(() => crypto.getHashes()); + +const socket = tls.connect({ port: 9, host: '127.0.0.1', rejectUnauthorized: false }); +socket.on('error', (err) => { + // Tests care that this is the right Node/OpenSSL-facing error, not a stale or + // unrelated OpenSSL stack entry. + assert(err.code || err.reason || err.message); +}); ``` -Boundary: +Failing tests that showed this class: ```text -OpenSSL C API - -> EdgeJS crypto/tls binding - -> Node Error object - -> JS test assertion +parallel/test-crypto-keygen-* +parallel/test-crypto-pqc-key-objects-* +parallel/test-webcrypto-derivebits-argon2 +parallel/test-webcrypto-sign-verify-eddsa +parallel/test-tls-hello-parser-failure +sequential/test-tls-connect ``` -Proposed solution: - -Use OpenSSL 3.5 for the WASIX OpenSSL dependency and map OpenSSL error stack -entries into Node-visible errors using the same reason/code selection native -Node expects. +Callgraph and boundary: -Sketch: +Current problematic path: -```cpp -unsigned long err = ERR_peek_last_error(); -int reason = ERR_GET_REASON(err); -return MakeNodeCryptoError(env, reason); +```text +JavaScript crypto/tls operation + -> EdgeJS src/internal_binding/binding_crypto.cc + -> OpenSSL API fails or reports a reason + -> EdgeJS MapOpenSslErrorCode()/CreateOpenSslError() + HERE IS THE PROBLEM: the binding can select the wrong error stack entry or + lack mappings for newer OpenSSL reasons, so JS sees the wrong code. + +JavaScript crypto operation requiring newer OpenSSL capability + -> EdgeJS OpenSSL dependency + HERE IS THE PROBLEM: older WASIX OpenSSL builds do not expose algorithms + that Node tests gate on OpenSSL 3.5 behavior. ``` -## Proposed Solution References +The boundary is EdgeJS <-> OpenSSL. Wasmer should preserve socket errors and +libc should expose `SO_ERROR`, but OpenSSL-to-Node error translation is owned by +EdgeJS. + +Proposed solution: -Proposed solution can be found in: +Update the WASIX OpenSSL dependency/build to the intended OpenSSL version and +make EdgeJS error mapping choose the relevant high-level OpenSSL reason for the +Node error being constructed. -**Reference commits:** +Relevant EdgeJS code paths: ```text -edgejs 5254f803 OpenSSL update to 3.5 -edgejs b9ef182f Fixes for OpenSSL error code mapping +~/src/edgejs/deps/openssl-wasix +~/src/edgejs/src/internal_binding/binding_crypto.cc +~/src/edgejs/src/crypto +~/src/edgejs/test/parallel/test-crypto-*.js +~/src/edgejs/test/parallel/test-tls-*.js ``` + +Proposed callgraph: + +```text +JavaScript crypto/tls operation + -> EdgeJS calls OpenSSL 3.5-capable dependency + -> OpenSSL reports failure/reason stack + -> EdgeJS selects the relevant reason for this operation + -> EdgeJS maps it to the Node-compatible code/reason/message + -> JavaScript observes the expected error class +``` + +This is intentionally not a generic Wasmer workaround: the runtime cannot know +which OpenSSL stack entry should become a Node.js error. + +## Proposed Solution References + +- edgejs `5254f803` `OpenSSL update to 3.5` +- edgejs `b9ef182f` `Fixes for OpenSSL error code mapping` diff --git a/plans/wasix-plan/edgejs/05_runner_determinism_and_workflow_setup.md b/plans/wasix-plan/edgejs/05_runner_determinism_and_workflow_setup.md index ebc342ded..827e19798 100644 --- a/plans/wasix-plan/edgejs/05_runner_determinism_and_workflow_setup.md +++ b/plans/wasix-plan/edgejs/05_runner_determinism_and_workflow_setup.md @@ -1,56 +1,105 @@ -# Runner Determinism and Workflow Setup +# EdgeJS: runner determinism and workflow setup Why this is a problem: -The test signal is meaningless if GitHub and local runs use different Wasmer -versions, different sysroots, different compiler backends, or broad host-path -mounts. WASIX behavior can change with libc and runtime versions. +The WASIX QuickJS Node suite is only useful if local and GitHub runs use the +same runtime backend, Wasmer version, sysroot, package mounts, and test root +rewrites. If one side silently uses a different sysroot or compiler backend, the +CSV starts mixing product regressions with harness drift. -Minimal manifestation: +This occurs in full-suite comparison runs, focused retries, and GitHub workflow +runs for `make test-wasix-quickjs-only`. + +Why native EdgeJS passes, and why WASIX still needs this: + +Native EdgeJS tests run directly against one host binary and one host OS. There +is no Wasmer compiler backend, sysroot tag, package mount list, or guest/host +path rewrite in the middle. The WASIX test path has all of those extra moving +parts, so a test can fail because CI used a different runtime, an older libc, a +missing mount, or a host path that is meaningless inside the guest. The EdgeJS +change is justified because the runner is EdgeJS test infrastructure: it must +make the WASIX environment deterministic enough that remaining failures can be +attributed to EdgeJS, libuv, `wasix-libc`, or Wasmer behavior rather than harness +drift. + +Minimal Example: ```sh -make test-wasix-quickjs-only +WASMER_BIN=~/src/wasmer/target/release/wasmer \ + make test-wasix-quickjs-only + +python3 test/tools/test.py \ + --timeout 2 \ + --test-root test \ + --shell ./scripts/edge-wasix-node-runner.sh \ + parallel/test-dns ``` -If the runner uses a different runtime backend or stale sysroot, failures can -look like runtime regressions when they are build-environment drift. +Failing-test evidence came from whole-suite columns where the same test changed +status only because the runner/sysroot/backend differed, especially DNS, UDP, +TLS, pseudo-tty, and stack-sensitive rows. -Proposed solution: +Callgraph and boundary: -- Install the intended Wasmer version explicitly. -- Install the intended `wasixcc` and sysroot. -- Force `wasmer run --llvm` for this lane. -- Mount only the directories required by the test package. -- Preserve `HOME=/tmp`, `NODE_TEST_DIR=/workspace/test`, SSL cert volume, and - `/etc` package volume. +Current problematic path: -Sketch: +```text +make test-wasix-quickjs-only + -> nodejs_test_harness + -> scripts/edge-wasix-node-runner.sh + -> wasmer run quickjs-wasm + HERE IS THE PROBLEM: backend, sysroot, Wasmer version, mounts, or host-path + rewrites can differ between local and CI. -```sh -curl https://get.wasmer.io -sSfL | sh -s "v7.2.0-alpha.3" - -wasmer run --llvm --net \ - --env HOME=/tmp \ - --env NODE_TEST_DIR=/workspace/test \ - --volume "$EDGEJS_ROOT:/workspace" \ - --volume "$EDGEJS_ROOT/quickjs-wasm/etc:/etc" \ - --volume "$EDGEJS_ROOT/ssl-certs:/usr/local/ssl" \ - --cwd /workspace \ - "$EDGEJS_ROOT/quickjs-wasm" -- "$test" +GitHub workflow + -> installs downloaded wasmer/wasixcc/sysroot + HERE IS THE PROBLEM: pre-release runtime and libc changes are hidden unless + the workflow pins exactly the intended versions or sources. ``` -## Proposed Solution References +The boundary is test harness/package configuration, not the JavaScript API. A +bad runner makes every area look suspicious. + +Proposed solution: + +Make the runner deterministic and visible: -Proposed solution can be found in: +- force `wasmer run --llvm`; +- pin/install the intended Wasmer pre-release in CI; +- build with the intended `wasix-libc` sysroot; +- mount only the directories needed by the guest; +- rewrite host test paths and reporter paths to guest paths; +- print the effective `wasmer run` command for debugging. -**Reference commits:** +Relevant EdgeJS code paths: ```text -edgejs f87e6848 Added node test runner for wasix quickjs -edgejs 2baeceea Added node test runner wasix quickjs to gh workflow -edgejs 352c55f3 Set wasmer version to 7.2.0-alpha.3 for wasix tests -edgejs c63d7771 Install wasmer 7.2.0-alpha.3 using curl and shell script... -edgejs fbbb21f7 Updated sysroot=v2026-05-26.1 wasixcc=v0.4.3 -edgejs 566e59a5 Force llvm runtime -edgejs 9aefbbe2 Isolate mapped host directories for wasix node test runner +~/src/edgejs/Makefile +~/src/edgejs/scripts/edge-wasix-node-runner.sh +~/src/edgejs/quickjs-wasm/build.sh +~/src/edgejs/quickjs-wasm/wasmer.toml +~/src/edgejs/.github/workflows/test-and-build-quickjs.yml ``` + +Proposed callgraph: + +```text +make test-wasix-quickjs-only + -> harness passes test path + reporter path + -> runner rewrites host paths to /workspace paths + -> runner invokes pinned WASMER_BIN with --llvm + -> quickjs-wasm package sees stable /workspace, /etc, /usr/local/ssl, HOME + -> CSV column reflects product behavior, not harness drift +``` + +This is an EdgeJS-owned test-infra plan. It should not change runtime semantics. + +## Proposed Solution References + +- edgejs `f87e6848` `Added note test runner for wasix quickjs` +- edgejs `2baeceea` `Added node test runner wasix quickjs to gh workflow` +- edgejs `352c55f3` `Set wasmer version to 7.2.0-alpha.3 for wasix tests` +- edgejs `c63d7771` `Install wasmer 7.2.0-alpha.3 using curl and shell script. Disable (temporarily other workflows).` +- edgejs `fbbb21f7` `Updated sysroot=v2026-05-26.1 wasixcc=v0.4.3` +- edgejs `566e59a5` `Force llvm runtime` +- edgejs `9aefbbe2` `Isolate mapped host directories for wasix node test runner` diff --git a/plans/wasix-plan/index.md b/plans/wasix-plan/index.md index 0b77b7533..c51ccecf5 100644 --- a/plans/wasix-plan/index.md +++ b/plans/wasix-plan/index.md @@ -25,7 +25,7 @@ already contains the behavior. The source comparison document is: ```text -/Users/sadhbh/src/dev/wasmer-workspace/docs/edgejs/006_acoose_vs_dunlewy_recovered_tests.md +~/src/wasmer-workspace/docs/edgejs/006_acoose_vs_dunlewy_recovered_tests.md ``` diff --git a/plans/wasix-plan/libuv-wasix/01_use_posix_spawn_on_wasix.md b/plans/wasix-plan/libuv-wasix/01_use_posix_spawn_on_wasix.md index 2b6f49260..04f185893 100644 --- a/plans/wasix-plan/libuv-wasix/01_use_posix_spawn_on_wasix.md +++ b/plans/wasix-plan/libuv-wasix/01_use_posix_spawn_on_wasix.md @@ -1,60 +1,85 @@ -# Use POSIX Spawn on WASIX +# libuv-wasix: use POSIX spawn on WASIX Why this is a problem: -WASI/WASIX does not provide `fork()` in the POSIX sense. libuv process spawn -must use a spawn-style API, then wait for process completion through the WASIX -join mechanism. Returning success from an unimplemented fork-like path creates -fake child handles and hangs. +WASIX does not provide `fork()`. The normal Unix libuv child-process path is +fork/exec oriented, so pretending that fork succeeded creates fake child handles, +missing stdout/stderr, and tests that hang while waiting for child output or an +exit event. -When it occurs: +This occurs whenever Node uses `child_process.spawn()`, `execFile()`, `exec()`, +`fork()`, or cluster worker creation on WASIX. -- `child_process.spawn()`; -- `child_process.execFile()`; -- Node self-spawn with `process.execPath`; -- proxy tests that spawn child clients. +Minimal Example: -Minimal manifestation: - -```js -const { spawn } = require('node:child_process'); -const child = spawn(process.execPath, ['-e', "console.log('child')"]); -child.stdout.on('data', b => process.stdout.write(b)); +```c +#include +#include + +static void on_exit(uv_process_t *req, int64_t status, int signal) { + printf("exit=%lld signal=%d\n", (long long)status, signal); + uv_close((uv_handle_t *)req, NULL); +} + +int main(void) { + uv_loop_t *loop = uv_default_loop(); + uv_process_t child; + char *args[] = { "edge", "-e", "console.log('child')", NULL }; + uv_process_options_t opts = {0}; + opts.file = "edge"; + opts.args = args; + opts.exit_cb = on_exit; + int rc = uv_spawn(loop, &child, &opts); + if (rc != 0) return 1; + return uv_run(loop, UV_RUN_DEFAULT); +} ``` -Boundary: +Callgraph and boundary: + +Current problematic path: ```text -Node child_process +Node child_process.spawn() -> libuv uv_spawn() - -> wasix-libc posix_spawn() - -> Wasmer proc_spawn3 - -> libuv process exit watcher via proc_join + -> libuv Unix fork/exec helper + HERE IS THE PROBLEM: fork is not available on WASIX; returning success or + leaving a fake pid makes the parent wait for child events that cannot occur. + -> wasix-libc/Wasmer never receive a valid process-spawn request ``` -Proposed solution: +The boundary is libuv <-> libc. libuv should call a POSIX-facing process API; +`wasix-libc` and Wasmer should own the WASIX syscall details. -Implement the libuv Unix process path with `posix_spawn()` and WASIX process -join primitives. Keep argv unchanged; do not base64-wrap multiline scripts in -EdgeJS. +Proposed solution: -Sketch: +Add a WASIX libuv process path that uses `posix_spawnp()` plus spawn file +actions. Do not emulate fork in libuv and do not rewrite EdgeJS JavaScript. -```c -int r = posix_spawnp(&pid, file, &file_actions, &attr, args, env); -if (r != 0) - return UV__ERR(r); +Relevant libuv-wasix code paths: -process->pid = pid; -uv__wasix_register_process_join(loop, process); +```text +~/src/edgejs/deps/libuv-wasix/src/unix/process.c +~/src/wasix-libc/libc-top-half/musl/src/process/posix_spawn.c +~/src/wasmer/lib/wasix/src/syscalls/wasix/proc_spawn*.rs +~/src/wasmer/lib/wasix/src/syscalls/wasix/proc_exec*.rs ``` -## Proposed Solution References - -Proposed solution can be found in: - -**Reference commits:** +Proposed callgraph: ```text -libuv-wasix ba06698b libuv-wasix: use WASIX posix_spawn/proc_join for child processes +Node child_process.spawn() + -> libuv uv_spawn() + -> libuv WASIX uv__spawn_and_init_child_posix_spawn_wasi() + -> posix_spawnp(file, file_actions, attrs, argv, envp) + -> wasix-libc proc_spawn3/proc_exec4 ABI with pointer arrays + -> Wasmer starts the child process and returns a real pid + -> libuv reports spawn/exit/close events normally ``` + +The minimal libuv piece is just translation from libuv's process options and +stdio container into `posix_spawn_file_actions_*` calls plus `posix_spawnp()`. + +## Proposed Solution References + +- libuv-wasix `ba06698b` `libuv-wasix: use WASIX posix_spawn/proc_join for child processes` diff --git a/plans/wasix-plan/libuv-wasix/02_udp_disconnect_and_multicast_option_compatibility.md b/plans/wasix-plan/libuv-wasix/02_udp_disconnect_and_multicast_option_compatibility.md index 4b94b24fc..d7d57d9e3 100644 --- a/plans/wasix-plan/libuv-wasix/02_udp_disconnect_and_multicast_option_compatibility.md +++ b/plans/wasix-plan/libuv-wasix/02_udp_disconnect_and_multicast_option_compatibility.md @@ -1,53 +1,95 @@ -# UDP Disconnect and Multicast Option Compatibility +# libuv-wasix: UDP disconnect and multicast option compatibility Why this is a problem: -Unix libuv uses idioms that are not currently exposed by WASIX exactly as on -Linux, such as `connect(AF_UNSPEC)` to disconnect UDP. Some multicast options -are also not implemented by the WASIX socket layer yet. +libuv's Unix UDP code relies on Linux behavior that is not fully available in +WASIX today. In particular, Linux disconnects a connected UDP socket with +`connect(AF_UNSPEC)`, and libuv exposes multicast option helpers that map to +platform `setsockopt()` calls. If WASIX returns `ENOTSUP`/`ENOSYS` for those +operations and libuv forwards that directly, Node dgram tests fail before they +reach the behavior they are trying to assert. -When it occurs: +This occurs in connected UDP, default-host send, multicast TTL/interface/loop, +and cluster/dgram tests. -- dgram disconnect tests; -- multicast TTL/interface/loopback tests; -- dgram option setters that expect support or a stable no-op. +Minimal Example: -Minimal manifestation: +```c +#include +#include + +int main(void) { + uv_loop_t *loop = uv_default_loop(); + uv_udp_t udp; + struct sockaddr_in addr; + uv_udp_init(loop, &udp); + uv_ip4_addr("127.0.0.1", 9, &addr); -```js -const dgram = require('node:dgram'); -const s = dgram.createSocket('udp4'); -s.setMulticastTTL(16); -s.disconnect(); + if (uv_udp_connect(&udp, (const struct sockaddr *)&addr) != 0) return 1; + if (uv_udp_connect(&udp, NULL) != 0) return 2; /* disconnect */ + if (uv_udp_set_multicast_ttl(&udp, 1) != 0) return 3; + return 0; +} ``` -Proposed solution: +Callgraph and boundary: -Prefer POSIX paths where WASIX supports them. Where WASIX lacks a specific -socket operation, add the smallest libuv WASIX adaptation that preserves the -libuv contract until the runtime exposes the real operation. +Current problematic path: -Sketch: +```text +Node dgram socket.disconnect() + -> libuv uv_udp_connect(handle, NULL) + -> Unix UDP disconnect path uses connect(AF_UNSPEC) + HERE IS THE PROBLEM: WASIX does not implement Linux AF_UNSPEC UDP + disconnect semantics, so libuv reports an operation failure. -```c -#if defined(__wasi__) - handle->flags &= ~UV_HANDLE_UDP_CONNECTED; - return 0; -#else - return connect(fd, (const struct sockaddr*) &unspec, sizeof(unspec)); -#endif +Node socket.setMulticastTTL()/setMulticastLoopback()/setMulticastInterface() + -> libuv multicast helper + -> setsockopt(multicast option) + HERE IS THE PROBLEM: some WASIX multicast options are not implemented yet, + so tests fail at option setup rather than packet behavior. ``` -For multicast options, return success only for options that are semantically -safe to no-op in the current package target. Otherwise return a real unsupported -error so tests do not hang behind a false success. +The boundary is libuv <-> POSIX-ish socket API. Long-term multicast packet +semantics belong in Wasmer virtual networking, but libuv still needs a sensible +WASIX compatibility path for options Node expects to call. -## Proposed Solution References +Proposed solution: + +For UDP disconnect, avoid the Linux-only `connect(AF_UNSPEC)` trick on WASIX and +update libuv's connected state consistently with the WASIX socket behavior. -Proposed solution can be found in: +For multicast option helpers, provide narrow WASIX shims that either succeed +when the option can be represented safely or return a stable, intentional error +for truly unsupported behavior. Do not patch EdgeJS dgram code. -**Reference commits:** +Relevant libuv-wasix code paths: ```text -libuv-wasix 0ae770b9 multicast TTL/loop/interface shims and UDP disconnect handling +~/src/edgejs/deps/libuv-wasix/src/unix/udp.c +~/src/edgejs/deps/libuv-wasix/src/uv-common.c +~/src/wasmer/lib/virtual-net/src/host.rs +~/src/wasmer/lib/virtual-net/src/client.rs ``` + +Proposed callgraph: + +```text +Node dgram socket.disconnect() + -> libuv uv_udp_connect(handle, NULL) + -> WASIX branch clears libuv connected state without AF_UNSPEC + -> later sends use unconnected UDP behavior + +Node multicast option call + -> libuv WASIX helper + -> supported option: update libuv/WASIX-compatible state and return 0 + -> unsupported option: return clear unsupported status without corrupting socket state +``` + +This keeps the temporary compatibility code in libuv, where Node's dgram API is +translated onto the platform socket API, while the full virtual-network +multicast implementation remains a Wasmer task. + +## Proposed Solution References + +- libuv-wasix `0ae770b9` `multicast TTL/loop/interface shims and UDP disconnect handling` diff --git a/plans/wasix-plan/libuv-wasix/03_tcp_keepalive_timing_options.md b/plans/wasix-plan/libuv-wasix/03_tcp_keepalive_timing_options.md index 53caa2203..7d8c280fb 100644 --- a/plans/wasix-plan/libuv-wasix/03_tcp_keepalive_timing_options.md +++ b/plans/wasix-plan/libuv-wasix/03_tcp_keepalive_timing_options.md @@ -1,51 +1,78 @@ -# TCP Keepalive Timing Options +# libuv-wasix: TCP keepalive timing options Why this is a problem: -Node and Undici call `uv_tcp_keepalive()`. On native platforms, libuv may set -`SO_KEEPALIVE` and then tune `TCP_KEEPIDLE`, `TCP_KEEPINTVL`, and -`TCP_KEEPCNT`. WASIX may accept `SO_KEEPALIVE` but reject the TCP timing knobs. -That turns harmless keepalive setup into a failed fetch or HTTP client setup. +Node and Undici call `uv_tcp_keepalive()` as part of normal HTTP/HTTPS client +setup. On Unix, libuv enables `SO_KEEPALIVE` and then often configures TCP +keepalive timing knobs such as `TCP_KEEPIDLE`, `TCP_KEEPINTVL`, and +`TCP_KEEPCNT`. WASIX may accept `SO_KEEPALIVE` but reject the timing knobs. If +libuv treats those timing failures as fatal, ordinary `fetch()` and keepalive +HTTP requests fail even though the socket itself is usable. -Minimal manifestation: +This occurs in fetch/HTTP/HTTPS client tests that configure keepalive. -```js -await fetch('http://127.0.0.1:12345/'); +Minimal Example: + +```c +#include + +int main(void) { + uv_loop_t *loop = uv_default_loop(); + uv_tcp_t tcp; + uv_tcp_init(loop, &tcp); + return uv_tcp_keepalive(&tcp, 1, 60) == 0 ? 0 : 1; +} ``` -Boundary: +Callgraph and boundary: + +Current problematic path: ```text -Undici fetch() - -> EdgeJS TCPWrap +JavaScript fetch()/HTTP client + -> Node TCPWrap::SetKeepAlive -> libuv uv_tcp_keepalive() - -> setsockopt(SO_KEEPALIVE) - -> setsockopt(IPPROTO_TCP, TCP_KEEPIDLE/INTVL/KEEPCNT) + -> libuv uv__tcp_keepalive() + -> setsockopt(SOL_SOCKET, SO_KEEPALIVE) + -> setsockopt(IPPROTO_TCP, TCP_KEEPIDLE/TCP_KEEPINTVL/TCP_KEEPCNT) + HERE IS THE PROBLEM: WASIX rejects timing knobs, and libuv propagates that + as failure for the whole keepalive operation. ``` +The boundary is libuv <-> socket options. Long-term, `wasix-libc`/Wasmer should +support the useful TCP options where possible. Until then, libuv should not make +unsupported timing knobs break basic keepalive enablement. + Proposed solution: -Until WASIX supports the TCP keepalive tuning knobs, make libuv's WASIX path -accept the keepalive request without issuing unsupported timing options. The -longer-term POSIX-complete answer is to implement these socket options in -`wasix-libc` and Wasmer. +Keep EdgeJS calling `uv_tcp_keepalive()` normally. In libuv-wasix, treat the +WASIX timing knobs as optional when `SO_KEEPALIVE` has been applied or when the +platform cannot expose those knobs yet. -Sketch: +Relevant libuv-wasix code paths: -```c -#if defined(__wasi__) || defined(__wasm32__) - return 0; -#else - return uv__tcp_keepalive(fd, on, delay); -#endif +```text +~/src/edgejs/deps/libuv-wasix/src/unix/tcp.c +~/src/edgejs/deps/libuv-wasix/src/unix/internal.h +~/src/wasix-libc/libc-bottom-half/cloudlibc/src/libc/sys/socket/setsockopt.c +~/src/wasmer/lib/wasix/src/syscalls/wasix/sock_set_opt_*.rs ``` -## Proposed Solution References - -Proposed solution can be found in: - -**Reference commits:** +Proposed callgraph: ```text -edgejs 9fa61f18 UV keep alive fix (disable unsupported options) +JavaScript fetch()/HTTP client + -> Node TCPWrap::SetKeepAlive + -> libuv uv_tcp_keepalive() + -> WASIX uv__tcp_keepalive() + -> enable/disable SO_KEEPALIVE when available + -> skip or tolerate unsupported TCP timing knobs + -> return success for the keepalive operation ``` + +This is deliberately small: make basic Node keepalive setup succeed without +claiming that WASIX already implements every TCP timing option. + +## Proposed Solution References + +- edgejs `9fa61f18` `UV keep alive fix (disable unsupported options)` diff --git a/plans/wasix-plan/napi/01_quickjs_structured_clone_serdes_for_sharedarraybuffer.md b/plans/wasix-plan/napi/01_quickjs_structured_clone_serdes_for_sharedarraybuffer.md index f6a7d230c..7510475ad 100644 --- a/plans/wasix-plan/napi/01_quickjs_structured_clone_serdes_for_sharedarraybuffer.md +++ b/plans/wasix-plan/napi/01_quickjs_structured_clone_serdes_for_sharedarraybuffer.md @@ -1,82 +1,92 @@ -# QuickJS Structured Clone / Serdes for SharedArrayBuffer +# N-API QuickJS: structured clone serdes for SharedArrayBuffer Why this is a problem: -Node worker and MessagePort internals serialize data between threads. The -QuickJS N-API serdes path must preserve `SharedArrayBuffer` backing storage and -identity semantics. Without this, worker bootstrap data can raise -`messageerror`, stall, or lose shared state. +Node workers and MessagePorts rely on structured clone. With V8, a +`SharedArrayBuffer` remains a `SharedArrayBuffer` after serialization and +deserialization, preserving shared memory identity for typed-array views. The +QuickJS N-API serializer cannot silently deserialize it as a plain `ArrayBuffer` +or drop it: Node internals use SharedArrayBuffer-backed counters during worker +bootstrap, and crypto/webcrypto worker tests depend on that shared state. -When it occurs: +This occurs when worker bootstrap data, MessagePort payloads, or crypto worker +messages carry `SharedArrayBuffer` or typed arrays backed by it. -- worker smoke tests; -- webcrypto worker/shared-buffer paths; -- Node internals using `SharedArrayBuffer` counters. - -Minimal manifestation: +Minimal Example: ```js +const assert = require('node:assert'); const { MessageChannel } = require('node:worker_threads'); + const { port1, port2 } = new MessageChannel(); +const sab = new SharedArrayBuffer(4); +new Uint32Array(sab)[0] = 42; -port1.on('message', (value) => { - console.log(value instanceof SharedArrayBuffer, value.byteLength); +port2.on('message', (value) => { + assert(value instanceof SharedArrayBuffer); + assert.strictEqual(new Uint32Array(value)[0], 42); }); - -port2.postMessage(new SharedArrayBuffer(4)); +port1.postMessage(sab); ``` -Boundary: +Representative failing tests: ```text -Node worker/message_port JS - -> N-API serializer - -> QuickJS JS_WriteObject2(JS_WRITE_OBJ_SAB | JS_WRITE_OBJ_REFERENCE) - -> N-API deserializer - -> QuickJS JS_ReadObject2(JS_READ_OBJ_SAB | JS_READ_OBJ_REFERENCE) +parallel/test-webcrypto-wrap-unwrap +parallel/test-crypto-key-objects-messageport +parallel/test-webcrypto-cryptokey-workers +worker bootstrap smoke using simple-echo-threads.cjs ``` -Proposed solution: - -Install QuickJS shared-array-buffer functions on the runtime and use -`JS_WriteObject2()` / `JS_ReadObject2()` with SAB/reference flags in the N-API -serdes implementation. - -Sketch: +Callgraph and boundary: -```cpp -JSSharedArrayBufferFunctions funcs{}; -funcs.sab_alloc = napi_shared_array_buffer__::alloc; -funcs.sab_free = napi_shared_array_buffer__::free; -funcs.sab_dup = napi_shared_array_buffer__::dup; -JS_SetSharedArrayBufferFunctions(rt, &funcs); - -JSValue bytes = JS_WriteObject2(ctx, obj, - JS_WRITE_OBJ_SAB | JS_WRITE_OBJ_REFERENCE); -JSValue value = JS_ReadObject2(ctx, data, len, - JS_READ_OBJ_SAB | JS_READ_OBJ_REFERENCE); -``` - -Keep allocation in an internal helper such as: +Current problematic path: ```text -napi/quickjs/src/internal/napi_shared_array_buffer.{h,cc} +JavaScript port.postMessage(sharedArrayBuffer) + -> Node worker/message_port layer + -> QuickJS N-API serializer in napi_serdes.cc + -> JS_WriteObject()/JS_ReadObject() + HERE IS THE PROBLEM: the basic QuickJS object writer does not carry the + SharedArrayBuffer table needed to reconstruct SAB values as SAB values. + -> receiver gets messageerror, wrong type, or non-shared backing storage ``` -Use `new (std::nothrow)[]` / `delete[]` style consistent with the codebase. +The boundary is N-API <-> QuickJS. EdgeJS should not inspect or rewrite worker +payloads; the JS engine integration must implement the structured-clone contract. -## Proposed Solution References +Proposed solution: -Proposed solution can be found in: +Install QuickJS `JSSharedArrayBufferFunctions` for allocation/refcounting and +use `JS_WriteObject2()` / `JS_ReadObject2()` with a `JSSABTab` when serializing +and deserializing values that may contain SharedArrayBuffer. -**Reference commits:** +Relevant N-API / QuickJS code paths: ```text -napi 21f22e3 QJS: Serdes for SharedArrayBuffer +~/src/edgejs/napi/quickjs/src/internal/napi_serdes.cc +~/src/edgejs/napi/quickjs/src/internal/napi_shared_array_buffer.h +~/src/edgejs/napi/quickjs/src/internal/napi_shared_array_buffer.cc +~/src/edgejs/napi/quickjs/src/unofficial_napi.cc +~/src/edgejs/napi/quickjs/deps/quickjs/quickjs.h ``` -**Cross-project pointer:** +Proposed callgraph: ```text -edgejs 38e01f79 Napi/Qjs: SharedArrayBuffer. Ssl certs in wasix tests. +JavaScript port.postMessage(sharedArrayBuffer) + -> Node worker/message_port layer + -> QuickJS N-API serializer + -> JS_WriteObject2(..., WRITE_OBJ_SAB, JSSABTab) + -> serialized payload records SAB identity/table entry + -> JS_ReadObject2(..., JSSABTab) + -> receiver gets a real SharedArrayBuffer backed by shared storage ``` + +The allocator/refcount helper belongs in N-API's QuickJS integration because it +bridges QuickJS SAB lifetime with Node/N-API ownership. + +## Proposed Solution References + +- napi `21f22e3` `QJS: Serdes for SharedArrayBuffer` +- edgejs `38e01f79` `Napi/Qjs: SharedArrayBuffer. Ssl certs in wasix tests.` diff --git a/plans/wasix-plan/napi/02_quickjs_napi_callsite_frames.md b/plans/wasix-plan/napi/02_quickjs_napi_callsite_frames.md index bff03cee3..669f17437 100644 --- a/plans/wasix-plan/napi/02_quickjs_napi_callsite_frames.md +++ b/plans/wasix-plan/napi/02_quickjs_napi_callsite_frames.md @@ -1,36 +1,91 @@ -# QuickJS/N-API Callsite Frames +# N-API QuickJS: callsite frames Why this is a problem: -Some Node internals inspect callsites to decide whether code is running inside -`node_modules`. If the QuickJS N-API path cannot expose enough callsite -information, behavior differs from V8. +EdgeJS uses callsite frames for Node-compatible stack formatting, caller +location, module path behavior, and diagnostics. V8 exposes structured stack +frames; the QuickJS N-API backend needs an equivalent enough representation. If +QuickJS returns only a string stack or missing frame metadata, tests that inspect +callsite behavior diverge from native EdgeJS. -When it occurs: +This occurs in console/error stack tests, diagnostics tests, and paths that need +caller file/line information. -- `test-buffer-constructor-node-modules-paths`; -- stack-dependent module and error behavior. - -Minimal manifestation: +Minimal Example: ```js -const err = new Error(); -Error.captureStackTrace(err); -console.log(err.stack); +const assert = require('node:assert'); + +function capture() { + const holder = {}; + Error.captureStackTrace(holder, capture); + assert.match(holder.stack, /capture/); + return holder.stack; +} + +const stack = capture(); +assert.match(stack, /\.js/); +``` + +Representative failing tests: + +```text +parallel/test-console-log-throw-primitive +parallel/test-console-no-swallow-stack-overflow +parallel/test-console-sync-write-error +parallel/test-diagnostics-channel-process +``` + +Callgraph and boundary: + +Current problematic path: + +```text +JavaScript Error.captureStackTrace()/diagnostics path + -> EdgeJS src/edge_util.cc asks unofficial N-API for callsites + -> napi/quickjs/src/unofficial_napi.cc + -> QuickJS stack data + HERE IS THE PROBLEM: QuickJS backend lacks V8-like callsite objects or does + not expose enough scriptName/lineNumber/columnNumber metadata to N-API. + -> EdgeJS receives incomplete caller/frame information ``` +The boundary is EdgeJS <-> unofficial N-API <-> QuickJS. EdgeJS can consume +callsites, but QuickJS/N-API must provide them. + Proposed solution: -Expose QuickJS callsite data through the N-API callsite hooks in the same shape -expected by EdgeJS internal helpers. +Add QuickJS intrinsic callsite objects and N-API helpers that return arrays of +frame objects with the fields EdgeJS expects: function name, script name/source +URL, line, column, and enough methods/properties to act like V8 callsites for +Node's internal use. -## Proposed Solution References +Relevant N-API / QuickJS code paths: -Proposed solution can be found in: +```text +~/src/edgejs/src/edge_util.cc +~/src/edgejs/napi/include/unofficial_napi.h +~/src/edgejs/napi/quickjs/src/unofficial_napi.cc +~/src/edgejs/napi/quickjs/src/internal/napi_callsite.cc +~/src/edgejs/napi/quickjs/deps/quickjs/quickjs.c +~/src/edgejs/napi/quickjs/deps/quickjs/quickjs.h +``` -**Reference commits mentioned in the recovery notes:** +Proposed callgraph: ```text -quickjs 41b00d4 Added callsites into QuickJS -napi 5a5c3b2 Improved quickjs callsites similar to v8 +JavaScript Error.captureStackTrace()/diagnostics path + -> EdgeJS src/edge_util.cc requests callsites + -> unofficial_napi_get_call_sites() + -> QuickJS JS_GetCurrentStackTrace() + -> QuickJS builds CallSite-like objects with file/line/column/function fields + -> EdgeJS formats/uses caller information like the V8 backend ``` + +This keeps stack-frame semantics in the JS engine integration, not in each +EdgeJS feature that happens to need caller metadata. + +## Proposed Solution References + +- quickjs `41b00d4` `Added callsites into QuickJS` +- napi `5a5c3b2` `Improved quickjs callsites similar to v8` diff --git a/plans/wasix-plan/quickjs/01_wasi_stack_limit_should_match_non_wasi_quickjs.md b/plans/wasix-plan/quickjs/01_wasi_stack_limit_should_match_non_wasi_quickjs.md index 46f1e8c57..d590c23bb 100644 --- a/plans/wasix-plan/quickjs/01_wasi_stack_limit_should_match_non_wasi_quickjs.md +++ b/plans/wasix-plan/quickjs/01_wasi_stack_limit_should_match_non_wasi_quickjs.md @@ -1,20 +1,17 @@ -# WASI Stack Limit Should Match Non-WASI QuickJS +# QuickJS: WASI stack limit should match non-WASI QuickJS Why this is a problem: -If QuickJS disables its stack limit under WASI, recursive JavaScript can run -until the Wasm stack exhausts. Then the runtime reports `RuntimeError: call -stack exhausted` instead of QuickJS producing the JS-facing overflow behavior -that Node tests expect. +QuickJS should own JavaScript stack overflow behavior. If the WASI build disables +QuickJS stack limits, deep JavaScript recursion falls through to the Wasm/native +runtime stack and appears as `RuntimeError: call stack exhausted`. Native +QuickJS/EdgeJS reports the JS-level stack overflow path instead, so Node tests +that assert stack/error behavior fail only on WASIX. -When it occurs: +This occurs in console, error, X509, and ttywrap tests that intentionally stress +stack handling or expect Node to catch/report stack overflow cleanly. -- console recursion tests; -- `test-x509-escaping`; -- `test-ttywrap-stack`; -- any test that intentionally exercises stack overflow handling. - -Minimal manifestation: +Minimal Example: ```js function recurse() { @@ -24,49 +21,67 @@ function recurse() { try { recurse(); } catch (err) { - console.log(err.name, err.message); + if (!/stack|recursion|call/i.test(String(err && err.message))) { + throw err; + } } ``` -Boundary: +Representative failing tests: ```text -JavaScript recursion - -> QuickJS stack check - -> JS RangeError/InternalError path - -> EdgeJS/Node error assertion - -Bad path: -JavaScript recursion - -> no QuickJS stack limit under WASI - -> Wasm stack exhaustion - -> Wasmer RuntimeError +parallel/test-console-log-throw-primitive +parallel/test-console-no-swallow-stack-overflow +parallel/test-console-sync-write-error +parallel/test-x509-escaping +parallel/test-ttywrap-stack ``` -Proposed solution: - -Remove the WASI-only `rt->stack_limit = 0` special case. Use the same stack -limit setup for WASI that non-WASI QuickJS uses. +Callgraph and boundary: -Sketch: +Current problematic path: -```c -/* Avoid WASI-only unlimited stack behavior. */ -JS_SetMaxStackSize(rt, stack_size); +```text +JavaScript recursive/error-heavy path + -> QuickJS function calls grow the JS stack + -> WASI-specific QuickJS setup has stack_limit disabled + HERE IS THE PROBLEM: QuickJS does not stop recursion at its JS stack limit, + so execution falls through to Wasm runtime stack exhaustion. + -> Wasmer reports RuntimeError: call stack exhausted + -> Node test sees the wrong error surface ``` -## Proposed Solution References +The boundary is QuickJS <-> runtime. Wasmer can report Wasm stack exhaustion, but +QuickJS should prevent normal JS recursion from reaching that boundary. -Proposed solution can be found in: +Proposed solution: -**Reference commits:** +Use the same QuickJS stack-limit mechanism for WASI as for non-WASI builds. The +WASI port may need a careful stack-base setter, but it should not special-case +`rt->stack_limit = 0` for ordinary EdgeJS execution. + +Relevant QuickJS code paths: ```text -quickjs 9a59f17 Set WASI stack limit same as non-WASI +~/src/edgejs/napi/quickjs/deps/quickjs/quickjs.c +~/src/edgejs/napi/quickjs/deps/quickjs/quickjs.h +~/src/edgejs/napi/quickjs/src/js_native_api_quickjs.cc ``` -**Related recovery note:** +Proposed callgraph: ```text -quickjs cec4427 Improved stack setter +JavaScript recursive/error-heavy path + -> QuickJS function calls grow the JS stack + -> QuickJS checks configured stack limit + -> QuickJS raises a JS-level stack overflow/internal error + -> EdgeJS/Node error handling observes the same class of behavior as native ``` + +This belongs in QuickJS because the JS engine owns stack accounting and the +shape of JavaScript stack overflow errors. + +## Proposed Solution References + +- quickjs `9a59f17` `Set WASI stack limit same as non-WASI` +- quickjs `cec4427` `Improved stack setter` diff --git a/plans/wasix-plan/wasix-libc/01_socket_type_flags_sock_nonblock_and_sock_cloexec.md b/plans/wasix-plan/wasix-libc/01_socket_type_flags_sock_nonblock_and_sock_cloexec.md index 819638b2a..9ac25c0e0 100644 --- a/plans/wasix-plan/wasix-libc/01_socket_type_flags_sock_nonblock_and_sock_cloexec.md +++ b/plans/wasix-plan/wasix-libc/01_socket_type_flags_sock_nonblock_and_sock_cloexec.md @@ -2,31 +2,43 @@ Why this is a problem: -POSIX allows callers to pass `SOCK_NONBLOCK` and `SOCK_CLOEXEC` in the socket -type. libuv does this on Unix: +In the baseline libc socket wrapper, the `type` argument can be forwarded to +WASIX without separating the base socket type from POSIX creation flags. That is +wrong because `SOCK_DGRAM | SOCK_NONBLOCK | SOCK_CLOEXEC` is not itself a base +socket type. The runtime wants to know `SOCK_DGRAM`; libc must apply +`SOCK_NONBLOCK` and `SOCK_CLOEXEC` as fd flags. + +If libc forwards the combined bits as the socket type, Wasmer may not recognize +the socket as datagram or stream. If libc opens the right socket but ignores the +flags, the fd remains blocking and code that expects `EAGAIN` can hang. + +Minimal Example: ```c -socket(AF_INET, SOCK_DGRAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); -``` +#include -If libc forwards those bits as the socket type, the runtime may not recognize -the type as `SOCK_DGRAM` or `SOCK_STREAM`. If libc ignores the flags, the fd can -be blocking and libuv's drain loops can hang. +int fd = socket(AF_INET, SOCK_DGRAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); +``` When it occurs: - UDP tests that should get `EAGAIN`; -- HTTP/TCP sockets created through libuv; -- any socket created via the standard Unix fast path. +- HTTP/TCP sockets created through Unix socket fast paths; +- any socket created with POSIX type flags. + +Callgraph and boundary: -Boundary: +Current problematic path: ```text -libuv uv__socket() - -> socket(domain, type | SOCK_NONBLOCK | SOCK_CLOEXEC, protocol) - -> wasix-libc socket() - -> __wasi_sock_open(domain, base_type, protocol, &fd) - -> fcntl(fd, F_SETFL, O_NONBLOCK) +C socket(AF_INET, SOCK_DGRAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0) + -> wasix-libc libc-top-half/musl/src/network/socket.c + -> wasix-libc libc-bottom-half/cloudlibc/src/libc/sys/socket/socket.c + -> may pass combined `ty` to __wasi_sock_open(...) + HERE IS THE PROBLEM: runtime sees type bits, not just SOCK_DGRAM + -> may not call fcntl(F_SETFL, O_NONBLOCK) + HERE IS THE PROBLEM: fd can remain blocking + -> Wasmer sock_open receives wrong type or a blocking fd ``` Proposed solution: @@ -34,6 +46,34 @@ Proposed solution: Mask the base socket type before calling WASIX, then apply flags through the normal fd flag paths. +Relevant wasix-libc code paths: + +```text +libc-top-half/musl/src/network/socket.c + POSIX-facing socket() wrapper + +libc-bottom-half/cloudlibc/src/libc/sys/socket/socket.c + lower socket(domain, base_type, protocol) to __wasi_sock_open(...) + strip SOCK_NONBLOCK and SOCK_CLOEXEC from type + apply O_NONBLOCK / FD_CLOEXEC after fd creation + +libc-bottom-half/cloudlibc/src/libc/fcntl/fcntl.c + fd flag application path used by fcntl(...) +``` + +Proposed callgraph: + +```text +C socket(AF_INET, SOCK_DGRAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0) + -> wasix-libc socket() + -> base_type = SOCK_DGRAM + -> flags = SOCK_NONBLOCK | SOCK_CLOEXEC + -> __wasi_sock_open(AF_INET, SOCK_DGRAM, IPPROTO_UDP, &fd) + -> fcntl(fd, F_SETFL, O_NONBLOCK) + -> fcntl(fd, F_SETFD, FD_CLOEXEC) + -> caller receives a datagram fd that is nonblocking and close-on-exec +``` + Sketch: ```c diff --git a/plans/wasix-plan/wasix-libc/02_boolean_socket_option_pointer_handling.md b/plans/wasix-plan/wasix-libc/02_boolean_socket_option_pointer_handling.md index 54ecf0283..f9eb775d1 100644 --- a/plans/wasix-plan/wasix-libc/02_boolean_socket_option_pointer_handling.md +++ b/plans/wasix-plan/wasix-libc/02_boolean_socket_option_pointer_handling.md @@ -2,10 +2,14 @@ Why this is a problem: -`setsockopt()` receives a pointer to the option value. If libc accidentally -interprets the pointer address as the option value, boolean options become -nondeterministic. A pointer address is almost always nonzero, so an intended -`0` can be treated as `1`. +In the baseline libc socket option wrapper, `setsockopt()` can treat `optval` as +the option value instead of reading the integer stored at `optval`. That makes +boolean options depend on the pointer address. Since a valid stack pointer is +almost always nonzero, a caller trying to set an option to `0` can accidentally +set it to `1`. + +The fix will make libc validate `optlen`, copy the pointed-to integer, and pass +that integer's boolean meaning to the WASIX socket option syscall. When it occurs: @@ -13,20 +17,26 @@ When it occurs: - keepalive toggles; - local-address/autoselect-family tests. -Minimal manifestation: +Minimal Example: ```c int off = 0; setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &off, sizeof(off)); ``` -Boundary: +Callgraph and boundary: + +Current problematic path: ```text -Node/net/libuv option setup - -> setsockopt(fd, level, name, &value, sizeof(value)) - -> wasix-libc reads pointed-to value - -> Wasmer sock_set_opt_flag/size/time +C setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &off, sizeof(off)) + -> wasix-libc libc-top-half/musl/src/network/setsockopt.c + -> wasix-libc libc-bottom-half/cloudlibc/src/libc/sys/socket/setsockopt.c + -> receives option_value = &off + -> may convert option_value pointer itself to boolean + HERE IS THE PROBLEM: stack address is nonzero even when `off == 0` + -> __wasi_sock_set_opt_flag(fd, IPV6_V6ONLY, true) + HERE IS THE PROBLEM: caller asked to disable the option, runtime enables it ``` Proposed solution: @@ -34,6 +44,31 @@ Proposed solution: Read the pointed-to integer/boolean value after validating `optlen`, then pass that value through the WASIX socket option syscall. +Relevant wasix-libc code paths: + +```text +libc-top-half/musl/src/network/setsockopt.c + POSIX-facing wrapper + +libc-bottom-half/cloudlibc/src/libc/sys/socket/setsockopt.c + validate option_len + memcpy integer value from option_value + lower boolean options to __wasi_sock_set_opt_flag(...) + lower size/time options to matching WASIX calls +``` + +Proposed callgraph: + +```text +C setsockopt(..., &off, sizeof(off)) where off == 0 + -> wasix-libc setsockopt() + -> validate option_len >= sizeof(int) + -> memcpy enabled from option_value + -> enabled = 0 + -> __wasi_sock_set_opt_flag(fd, IPV6_V6ONLY, false) + -> runtime receives the caller's intended value +``` + Sketch: ```c diff --git a/plans/wasix-plan/wasix-libc/03_getsockopt_so_error.md b/plans/wasix-plan/wasix-libc/03_getsockopt_so_error.md index e93191df2..5e03d9732 100644 --- a/plans/wasix-plan/wasix-libc/03_getsockopt_so_error.md +++ b/plans/wasix-plan/wasix-libc/03_getsockopt_so_error.md @@ -2,11 +2,16 @@ Why this is a problem: -libuv uses `SO_ERROR` to turn nonblocking socket completion into the correct -JavaScript error. If libc does not expose the runtime's last socket error, -EdgeJS cannot report Node-compatible errors. +In the baseline libc wrapper, `getsockopt(SOL_SOCKET, SO_ERROR)` does not expose +Wasmer's socket last-error state through normal POSIX `getsockopt()` semantics. +That means a C caller can complete a nonblocking connect, ask for `SO_ERROR`, +and fail to receive the actual pending socket error. -Minimal manifestation: +The fix will translate `SO_ERROR` into the WASIX last-error socket option, +return the result as an `int`, and update `*optlen` the way POSIX callers +expect. + +Minimal Example: ```c int err = 0; @@ -14,11 +19,52 @@ socklen_t len = sizeof(err); getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &len); ``` +Callgraph and boundary: + +Current problematic path: + +```text +C getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &len) + -> wasix-libc libc-top-half/musl/src/network/getsockopt.c + -> wasix-libc libc-bottom-half/cloudlibc/src/libc/sys/socket/getsockopt.c + -> no correct SO_ERROR mapping to WASIX LastError + HERE IS THE PROBLEM: libc cannot ask runtime for pending socket error + -> caller receives success/unsupported/wrong value + HERE IS THE PROBLEM: POSIX SO_ERROR contract is not met +``` + Proposed solution: Translate `SOL_SOCKET/SO_ERROR` into the WASIX last-error socket option, return an `int`, and keep normal `getsockopt()` pointer/length semantics. +Relevant wasix-libc code paths: + +```text +libc-top-half/musl/src/network/getsockopt.c + POSIX-facing wrapper + +libc-bottom-half/cloudlibc/src/libc/sys/socket/getsockopt.c + detect SOL_SOCKET + SO_ERROR + call __wasi_sock_get_opt_size(fd, Sockoption::LastError, &value) + copy int value into optval and set *optlen + +libc-bottom-half/headers/public/wasi/api_wasix.h + __wasi_sock_get_opt_size(...) +``` + +Proposed callgraph: + +```text +C getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &len) + -> wasix-libc getsockopt() + -> recognize SO_ERROR + -> __wasi_sock_get_opt_size(fd, LastError, &last_error) + -> err = (int)last_error + -> len = sizeof(int) + -> caller receives pending socket error through POSIX API +``` + ## Proposed Solution References Proposed solution can be found in: diff --git a/plans/wasix-plan/wasix-libc/04_posix_st_mode_for_files_and_directories.md b/plans/wasix-plan/wasix-libc/04_posix_st_mode_for_files_and_directories.md index ba00d493e..59798d0c2 100644 --- a/plans/wasix-plan/wasix-libc/04_posix_st_mode_for_files_and_directories.md +++ b/plans/wasix-plan/wasix-libc/04_posix_st_mode_for_files_and_directories.md @@ -2,32 +2,56 @@ Why this is a problem: -Node inspects POSIX `st_mode` bits to determine whether a path is a regular -file, directory, executable, or readable/writable. WASI file metadata does not -automatically look like POSIX mode bits. +In the baseline libc stat conversion, WASI file metadata can be exposed without +normal POSIX `st_mode` type and permission bits. C code expects `S_ISREG()` and +`S_ISDIR()` to work. If libc leaves those bits empty or incomplete, callers see +ordinary files and directories as mode `0`-like objects. + +The fix will synthesize POSIX type bits and conservative permission bits during +WASI-to-`struct stat` conversion. This belongs in libc, not in each guest +program after it calls `stat()`. When it occurs: - FastUTF8 stream stat tests; - module loader file checks; -- any Node code calling `fs.stat()`. +- any C or JS runtime code calling `stat()`, `fstat()`, or `fstatat()`. + +Minimal Example: + +```c +#include +#include -Minimal manifestation: +int main(int argc, char **argv) { + const char *path = argc > 1 ? argv[1] : argv[0]; + struct stat st; -```js -const fs = require('node:fs'); -const st = fs.statSync(__filename); -console.log((st.mode & 0o170000).toString(8)); // should show regular-file type + if (stat(path, &st) != 0) { + perror("stat"); + return 1; + } + + printf("mode: %o\n", st.st_mode); + printf("is regular: %d\n", S_ISREG(st.st_mode)); + printf("is dir: %d\n", S_ISDIR(st.st_mode)); +} ``` -Boundary: +Callgraph and boundary: + +Current problematic path: ```text -Node fs.stat() - -> libuv uv_fs_stat() - -> wasix-libc stat() - -> WASI fd_filestat_get/path_filestat_get - -> POSIX st_mode synthesis +C stat(path, &st) + -> wasix-libc libc-bottom-half/sources/posix.c __wasilibc_stat(...) + -> __wasi_path_filestat_get(...) or __wasi_fd_filestat_get(...) + -> wasix-libc stat conversion + -> maps WASI filetype incompletely into st.st_mode + HERE IS THE PROBLEM: S_IFREG/S_IFDIR or useful permission bits are absent + -> C caller checks S_ISREG(st.st_mode) + -> false for a normal file + HERE IS THE PROBLEM: POSIX stat contract is broken at libc boundary ``` Proposed solution: @@ -35,6 +59,35 @@ Proposed solution: Synthesize POSIX type bits and conservative permission bits in `wasix-libc`. Do not normalize this in EdgeJS after the fact. +Relevant wasix-libc code paths: + +```text +libc-bottom-half/sources/posix.c + __wasilibc_stat(...) + calls WASI filestat functions + +libc-bottom-half/cloudlibc/src/libc/sys/stat/stat_impl.h + to_public_stat(...) + convert __wasi_filestat_t to struct stat + set S_IFDIR/S_IFREG and default permission bits + +libc-bottom-half/cloudlibc/src/libc/sys/stat/fstat.c +libc-bottom-half/cloudlibc/src/libc/sys/stat/fstatat.c + share the same conversion helper +``` + +Proposed callgraph: + +```text +C stat(path, &st) + -> wasix-libc __wasilibc_stat(...) + -> __wasi_path_filestat_get(...) + -> to_public_stat(...) + -> if WASI filetype is regular, st_mode |= S_IFREG | 0600 + -> if WASI filetype is directory, st_mode |= S_IFDIR | 0700 + -> C caller sees S_ISREG/S_ISDIR behave normally +``` + Sketch: ```c diff --git a/plans/wasix-plan/wasix-libc/05_loopback_reverse_lookup.md b/plans/wasix-plan/wasix-libc/05_loopback_reverse_lookup.md index 7d0e3c31b..834d6c0f7 100644 --- a/plans/wasix-plan/wasix-libc/05_loopback_reverse_lookup.md +++ b/plans/wasix-plan/wasix-libc/05_loopback_reverse_lookup.md @@ -2,24 +2,55 @@ Why this is a problem: -POSIX applications expect loopback reverse lookup and `getnameinfo()` to return -`localhost` in ordinary environments. The guest should not need EdgeJS-specific -hard-coded answers for `127.0.0.1` or `::1`. +In the baseline resolver path, `getnameinfo()` can fail to return `localhost` +for IPv4 or IPv6 loopback addresses. That pushes guests toward hard-coded +application-level loopback answers, which is the wrong layer. A POSIX-like libc +should provide ordinary loopback reverse lookup behavior itself, using +`/etc/hosts` where available and a small loopback fallback where appropriate. -Minimal manifestation: +The fix will make `getnameinfo()` recognize loopback addresses by family and +return `localhost` when no hosts-file result has already filled the name. -```js -const dns = require('node:dns'); -dns.reverse('127.0.0.1', (err, names) => console.log(err, names)); +Minimal Example: + +```c +#include +#include +#include +#include +#include + +int main(void) { + struct sockaddr_in addr = {0}; + addr.sin_family = AF_INET; + addr.sin_port = htons(80); + inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr); + + char host[NI_MAXHOST]; + int err = getnameinfo((struct sockaddr *)&addr, sizeof(addr), + host, sizeof(host), NULL, 0, NI_NAMEREQD); + if (err != 0) { + printf("getnameinfo: %s\n", gai_strerror(err)); + return 1; + } + + printf("host: %s\n", host); +} ``` -Boundary: +Callgraph and boundary: + +Current problematic path: ```text -Node dns.reverse() - -> c-ares / getnameinfo-shaped resolver behavior - -> wasix-libc getnameinfo() - -> /etc/hosts or loopback fallback +C getnameinfo(127.0.0.1, NI_NAMEREQD) + -> wasix-libc libc-top-half/musl/src/network/getnameinfo.c + -> reverse_hosts(...) + -> /etc/hosts may be unavailable or may not fill result + -> no loopback fallback by family + HERE IS THE PROBLEM: 127.0.0.1/::1 can remain unnamed + -> getnameinfo returns failure or numeric fallback + HERE IS THE PROBLEM: guest does not get normal localhost reverse lookup ``` Proposed solution: @@ -28,6 +59,30 @@ In libc resolver code, switch on address family and return `localhost` for IPv4 and IPv6 loopback. Also support a mounted `/etc/hosts`; do not create synthetic hosts files in Wasmer. +Relevant wasix-libc code paths: + +```text +libc-top-half/musl/src/network/getnameinfo.c + getnameinfo(...) + after reverse_hosts(...), if host buffer is empty, call reverse_loopback(...) + + reverse_loopback(...) + switch on AF_INET / AF_INET6 + map 127.0.0.1 and ::1 to localhost +``` + +Proposed callgraph: + +```text +C getnameinfo(127.0.0.1, NI_NAMEREQD) + -> wasix-libc getnameinfo(...) + -> reverse_hosts(...) + -> if result is still empty, reverse_loopback(...) + -> family == AF_INET and addr == 127.0.0.1 + -> host = "localhost" + -> caller receives localhost +``` + Sketch: ```c diff --git a/plans/wasix-plan/wasmer/01_udp_datagram_receive_readiness_and_backlog.md b/plans/wasix-plan/wasmer/01_udp_datagram_receive_readiness_and_backlog.md index 01778e0d3..305e3dcfd 100644 --- a/plans/wasix-plan/wasmer/01_udp_datagram_receive_readiness_and_backlog.md +++ b/plans/wasix-plan/wasmer/01_udp_datagram_receive_readiness_and_backlog.md @@ -2,50 +2,94 @@ Why this is a problem: -UDP readiness is datagram-based, not byte-stream-based. A readiness probe must -not consume a packet that the guest later expects to receive. If `poll_oneoff()` -or the virtual socket readiness path internally calls a destructive receive, the -runtime can report `FdRead` and then make the guest's later `recvfrom()` see -`EAGAIN` or block forever. +In the baseline runtime, UDP readiness can be implemented by touching the real +socket receive path. That is unsafe for UDP. A datagram is the unit of delivery, +and observing readiness must not consume or discard the packet that made the fd +readable. + +The broken behavior is that `poll_oneoff()` can report `FdRead`, but the later +`recvfrom()` can see `EAGAIN`, block, or time out because the readiness path +already consumed the datagram. Zero-length UDP packets expose this quickly, +because there is no payload byte to distinguish "empty datagram delivered" from +"nothing was available" unless the runtime preserves the datagram boundary. + +The fix will make readiness non-destructive. Wasmer should keep a per-socket UDP +backlog. Readiness probes may fill that backlog, but guest receive calls must be +the only path that consumes it. `peek` reads should copy from the backlog without +popping it. When it occurs: - zero-length UDP packets; - recursive UDP send/receive callbacks; - default-host dgram tests; -- any path where libuv polls first and then drains the socket. +- any path where the guest polls first and then drains the socket. -Minimal manifestation: +Minimal Example: -```js -const dgram = require('node:dgram'); -const s = dgram.createSocket('udp4'); +```c +#include +#include +#include +#include +#include +#include +#include -s.on('message', (msg) => { - console.log('received', msg.length); - s.close(); -}); +int main(void) { + int fd = socket(AF_INET, SOCK_DGRAM | SOCK_NONBLOCK, 0); -s.bind(0, () => { - const port = s.address().port; - s.send(Buffer.alloc(0), port, '127.0.0.1'); -}); + struct sockaddr_in addr = {0}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr.sin_port = 0; + bind(fd, (struct sockaddr *)&addr, sizeof(addr)); + + socklen_t len = sizeof(addr); + getsockname(fd, (struct sockaddr *)&addr, &len); + + sendto(fd, "", 0, 0, (struct sockaddr *)&addr, sizeof(addr)); + + struct pollfd pfd = {.fd = fd, .events = POLLIN}; + poll(&pfd, 1, 1000); + + char byte; + ssize_t n = recvfrom(fd, &byte, sizeof(byte), 0, NULL, NULL); + if (n < 0) + perror("recvfrom after poll"); + else + printf("received datagram length: %zd\n", n); + + close(fd); +} ``` -Expected call path: +Callgraph and boundary: + +Current problematic path: ```text -JS dgram.send() - -> libuv uv_udp_send() - -> wasix-libc sendto()/sendmsg() +C sendto(fd, "", 0, ...) + -> wasix-libc sendto() -> Wasmer sock_send_to() - -> virtual-net UDP socket - -libuv poll - -> wasix-libc poll_oneoff() - -> Wasmer poll_oneoff() - -> socket readiness - -> later sock_recv_from() + -> virtual-net UDP socket receives one zero-length datagram + +C poll([{ fd, POLLIN }]) + -> wasix-libc poll() + -> __wasi_poll_oneoff() + -> Wasmer lib/wasix/src/syscalls/wasi/poll_oneoff.rs + -> Wasmer socket readiness path + -> virtual-net LocalUdpSocket/RemoteUdpSocket readiness + -> may call recv_from()/peek-like logic against the real socket + HERE IS THE PROBLEM: readiness can consume or lose the datagram + +C recvfrom(fd, ...) + -> wasix-libc recvfrom() + -> Wasmer sock_recv_from() + -> Wasmer lib/wasix/src/net/socket.rs InodeSocket::recv_from(...) + -> virtual-net try_recv_from(...) + -> datagram is already gone + HERE IS THE PROBLEM: guest sees EAGAIN, timeout, or missing packet ``` Proposed solution: @@ -55,6 +99,48 @@ observe the socket. `poll_read_ready()` should answer from backlog first. A real guest receive should pop from backlog; a guest peek should copy from backlog without popping. +Relevant Wasmer code paths: + +```text +lib/wasix/src/syscalls/wasi/poll_oneoff.rs + poll_oneoff_internal(...) + reports fd-read readiness without consuming guest-visible data + +lib/wasix/src/net/socket.rs + InodeSocket::recv_from(...) + read from UDP backlog first + only pop backlog on non-peek receive + +lib/virtual-net/src/host.rs + LocalUdpSocket::try_recv_from(...) + LocalUdpSocket readiness / poll_read_ready path + fill backlog once, do not throw datagram away + +lib/virtual-net/src/client.rs + RemoteUdpSocket::try_recv_from(...) + mirror host-side backlog behavior for remote virtual networking +``` + +Proposed callgraph: + +```text +C sendto(fd, "", 0, ...) + -> Wasmer sock_send_to() + -> virtual-net queues one zero-length UDP datagram + +C poll([{ fd, POLLIN }]) + -> Wasmer poll_oneoff_internal(...) + -> UDP readiness checks backlog + -> if backlog is empty, receive exactly one datagram into backlog + -> return FdRead without consuming backlog + +C recvfrom(fd, ...) + -> Wasmer InodeSocket::recv_from(...) + -> virtual-net returns datagram from backlog + -> backlog pops only because this is a non-peek receive + -> guest observes received datagram length 0 +``` + Sketch: ```rust diff --git a/plans/wasix-plan/wasmer/02_connected_udp_send_peer_state_and_emsgsize.md b/plans/wasix-plan/wasmer/02_connected_udp_send_peer_state_and_emsgsize.md index d649f07da..a9969fe92 100644 --- a/plans/wasix-plan/wasmer/02_connected_udp_send_peer_state_and_emsgsize.md +++ b/plans/wasix-plan/wasmer/02_connected_udp_send_peer_state_and_emsgsize.md @@ -2,9 +2,27 @@ Why this is a problem: -Connected UDP is still UDP. A vectored send represents one datagram, not one -datagram per iovec. The runtime also needs to remember the peer chosen by -`connect()` so `getpeername()` and connected sends behave normally. +In the baseline runtime, connected UDP loses two pieces of POSIX socket +semantics. + +First, `connect()` can auto-bind or upgrade a UDP pre-socket, but the resulting +Wasmer socket state does not reliably keep the concrete connected socket and its +peer address together. After that, `getpeername()` can report `ENOTCONN` or an +empty peer even though `connect()` succeeded, and connected sends may run through +a path that no longer knows which peer should receive the datagram. + +Second, vectored writes to a connected UDP socket can be treated like stream +writes: each iovec can be sent separately. That is wrong for UDP. A single +`writev()` / `sendmsg()` call on a connected UDP socket represents one datagram +containing the concatenated iovec bytes. If the runtime sends one datagram per +iovec, the receiver observes `"he"` and `"llo"` as separate packets instead of +one `"hello"` packet. + +The fix makes Wasmer preserve the connected UDP peer after `connect()`, make +`getpeername()` read that stored peer, and make connected vectored UDP send +coalesce all iovecs into one datagram before calling the virtual network layer. +Oversized coalesced packets should fail as `EMSGSIZE` / `Errno::Msgsize`, not as +a generic I/O error. When it occurs: @@ -13,36 +31,135 @@ When it occurs: - `test-dgram-msgsize`; - c-ares connected UDP resolver paths. -Minimal manifestation: - -```js -const dgram = require('node:dgram'); -const s = dgram.createSocket('udp4'); - -s.bind(0, () => { - s.connect(s.address().port, '127.0.0.1', () => { - s.send(['he', 'llo']); // one UDP datagram: "hello" - }); -}); +Minimal Example: + +```c +#include +#include +#include +#include +#include +#include + +int main(void) { + int receiver = socket(AF_INET, SOCK_DGRAM, 0); + int sender = socket(AF_INET, SOCK_DGRAM, 0); + + struct sockaddr_in addr = {0}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr.sin_port = 0; + bind(receiver, (struct sockaddr *)&addr, sizeof(addr)); + + socklen_t len = sizeof(addr); + getsockname(receiver, (struct sockaddr *)&addr, &len); + connect(sender, (struct sockaddr *)&addr, sizeof(addr)); + + struct sockaddr_in peer = {0}; + socklen_t peer_len = sizeof(peer); + getpeername(sender, (struct sockaddr *)&peer, &peer_len); + printf("connected UDP peer port: %u\n", ntohs(peer.sin_port)); + + struct iovec iov[2] = { + {.iov_base = "he", .iov_len = 2}, + {.iov_base = "llo", .iov_len = 3}, + }; + writev(sender, iov, 2); + + char buf[16] = {0}; + ssize_t n = recvfrom(receiver, buf, sizeof(buf), 0, NULL, NULL); + printf("received %zd bytes: %.*s\n", n, (int)n, buf); + + close(sender); + close(receiver); +} ``` Callgraph and boundary: +Current problematic path: + ```text -Node dgram array send - -> libuv uv_udp_send() - -> wasix-libc sendmsg()/send() - -> Wasmer connected UDP send - -> virtual-net send_to(peer, coalesced_iovs) +C connect(sender, peer) + -> wasix-libc connect() + -> Wasmer lib/wasix/src/net/socket.rs InodeSocket::connect(...) + -> for an existing UdpSocket, peer may be stored as `peer: Option` + -> for an auto-bound/upgraded UDP socket, the concrete socket can be lost + or the old pre-socket can remain visible + HERE IS THE PROBLEM: connected UDP state is split or missing + +C getpeername(sender) + -> wasix-libc getpeername()/sock_addr_peer + -> Wasmer lib/wasix/src/net/socket.rs InodeSocket::addr_peer() + -> may see no stored UDP peer + -> may fall through to virtual-net addr_peer() returning None/NotConnected + HERE IS THE PROBLEM: getpeername() can report ENOTCONN after connect() + +C writev(sender, ["he", "llo"]) + -> wasix-libc writev()/sendmsg-shaped path + -> Wasmer connected UDP send path + -> each iovec can be treated like an independent send buffer + -> virtual-net try_send_to("he", peer) + -> virtual-net try_send_to("llo", peer) + HERE IS THE PROBLEM: receiver observes two UDP datagrams, not one ``` Proposed solution: -- During UDP `connect()`, preserve the upgraded/auto-bound socket. -- Store the connected peer in socket state. -- For connected UDP vectored send, concatenate iovecs into a single datagram. +- During UDP `connect()`, if the runtime upgrades or auto-binds the socket, + keep the upgraded socket entry instead of falling back to the old pre-socket. +- Store the connected peer address in Wasmer socket state. +- Implement `addr_peer()` / `getpeername()` for connected UDP by returning the + stored peer. +- For connected UDP vectored send, concatenate iovecs into one temporary packet + and send exactly one datagram to the stored peer. - Map oversized datagrams to `Errno::Msgsize`, not a generic I/O error. +Relevant Wasmer code paths: + +```text +lib/wasix/src/net/socket.rs + InodeSocket::connect(...) + preserve the concrete UDP socket and set `peer: Some(peer)` + + InodeSocket::addr_peer(...) + for InodeSocketKind::UdpSocket, return the stored peer first + + connected UDP send path / sock_send_msg path + coalesce iovecs before calling virtual-net + map NetworkError::MessageSize to Errno::Msgsize + +lib/virtual-net/src/host.rs + LocalUdpSocket::addr_peer(...) + LocalUdpSocket::try_send_to(...) + +lib/virtual-net/src/client.rs + RemoteUdpSocket::addr_peer(...) + RemoteUdpSocket::try_send_to(...) +``` + +Proposed callgraph: + +```text +C connect(sender, peer) + -> wasix-libc connect() + -> Wasmer InodeSocket::connect(...) + -> keep upgraded/bound UDP socket + -> set socket peer = Some(peer) + +C getpeername(sender) + -> wasix-libc getpeername()/sock_addr_peer + -> Wasmer InodeSocket::addr_peer() + -> return socket peer = Some(peer) + +C writev(sender, ["he", "llo"]) + -> wasix-libc writev()/sendmsg-shaped path + -> Wasmer connected UDP send path + -> coalesce iovecs into "hello" + -> virtual-net try_send_to("hello", peer) + -> receiver gets exactly one datagram +``` + Sketch: ```rust diff --git a/plans/wasix-plan/wasmer/03_last_socket_error_so_error.md b/plans/wasix-plan/wasmer/03_last_socket_error_so_error.md index 5cb300c31..568c46775 100644 --- a/plans/wasix-plan/wasmer/03_last_socket_error_so_error.md +++ b/plans/wasix-plan/wasmer/03_last_socket_error_so_error.md @@ -2,32 +2,80 @@ Why this is a problem: -Node and libuv check `getsockopt(SO_ERROR)` after nonblocking connect and write -paths. If Wasmer loses the actual socket error, JavaScript sees the wrong error, -for example `EPIPE` or `ECONNRESET` where the test expects `ECONNREFUSED`. +In the baseline runtime, the socket operation that actually fails and the later +`getsockopt(SO_ERROR)` call can be disconnected from each other. A nonblocking +connect can fail with `ECONNREFUSED`, but if Wasmer does not record that error +on the socket object, the next `SO_ERROR` read can return success or a generic +error. + +The visible result is wrong POSIX error reporting. Code that expects +`ECONNREFUSED` may instead see `EPIPE`, `ECONNRESET`, or no pending error at +all. The fix will store the last socket error in Wasmer socket state and expose +that state through the WASIX `Sockoption::LastError` path used by +`getsockopt(SO_ERROR)`. When it occurs: - refused HTTP/TLS connects; - proxy connection-refused tests; -- tests asserting exact Node error codes. +- tests asserting exact connect or write error codes. -Minimal manifestation: +Minimal Example: -```js -const net = require('node:net'); +```c +#include +#include +#include +#include +#include +#include +#include -net.connect({ host: '127.0.0.1', port: 9 }) - .on('error', (err) => console.log(err.code)); +int main(void) { + int fd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0); + + struct sockaddr_in addr = {0}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr.sin_port = htons(9); /* normally closed discard port */ + + connect(fd, (struct sockaddr *)&addr, sizeof(addr)); + + struct pollfd pfd = {.fd = fd, .events = POLLOUT}; + poll(&pfd, 1, 1000); + + int err = 0; + socklen_t errlen = sizeof(err); + getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &errlen); + printf("SO_ERROR: %s\n", strerror(err)); + + close(fd); +} ``` -Boundary: +Callgraph and boundary: + +Current problematic path: ```text -libuv uv__stream_connect() - -> getsockopt(fd, SOL_SOCKET, SO_ERROR, ...) - -> wasix-libc __wasi_sock_get_opt_*() - -> Wasmer socket last_error +C connect(fd, closed_loopback_port) + -> wasix-libc connect() + -> Wasmer lib/wasix/src/net/socket.rs InodeSocket::connect(...) + -> virtual-net TCP connect + -> host OS reports ECONNREFUSED + -> Wasmer maps the immediate operation error + HERE IS THE PROBLEM: error may not be retained as socket last_error + +C poll(fd, POLLOUT) + -> readiness completes because connection attempt is finished + +C getsockopt(fd, SOL_SOCKET, SO_ERROR) + -> wasix-libc getsockopt() + -> __wasi_sock_get_opt_size(Sockoption::LastError) + -> Wasmer lib/wasix/src/syscalls/wasix/sock_get_opt_size.rs + -> socket.last_error() + -> no preserved error, success, or wrong generic error + HERE IS THE PROBLEM: caller cannot recover original ECONNREFUSED ``` Proposed solution: @@ -36,6 +84,44 @@ Record the last connect/send/recv error on the Wasmer socket object and expose it through the WASIX socket-option path. Reading `SO_ERROR` should return and, where POSIX requires it, clear the pending error. +Relevant Wasmer code paths: + +```text +lib/wasix/src/net/socket.rs + InodeSocket::connect(...) + InodeSocket::send(...) + InodeSocket::recv_from(...) + record meaningful socket errors into socket state + + InodeSocket::last_error(...) + return the stored socket error as Errno + +lib/wasix/src/syscalls/wasix/sock_get_opt_size.rs + Sockoption::LastError + return socket.last_error() + +lib/virtual-net/src/host.rs + TcpStream/UDP socket error mapping + +lib/virtual-net/src/lib.rs + VirtualSocket::last_error(...) +``` + +Proposed callgraph: + +```text +C connect(fd, closed_loopback_port) + -> Wasmer InodeSocket::connect(...) + -> virtual-net returns NetworkError::ConnectionRefused + -> Wasmer maps to Errno::Connrefused and records socket.last_error + +C getsockopt(fd, SOL_SOCKET, SO_ERROR) + -> wasix-libc translates SO_ERROR to WASIX LastError + -> Wasmer sock_get_opt_size(Sockoption::LastError) + -> InodeSocket::last_error() + -> returns ECONNREFUSED to C caller +``` + Sketch: ```rust diff --git a/plans/wasix-plan/wasmer/04_proc_spawn3_proc_exec4_for_real_argv_envp.md b/plans/wasix-plan/wasmer/04_proc_spawn3_proc_exec4_for_real_argv_envp.md index a225e81e4..b55d382ef 100644 --- a/plans/wasix-plan/wasmer/04_proc_spawn3_proc_exec4_for_real_argv_envp.md +++ b/plans/wasix-plan/wasmer/04_proc_spawn3_proc_exec4_for_real_argv_envp.md @@ -2,38 +2,67 @@ Why this is a problem: -Older WASIX process syscalls encode `argv` and `envp` as newline-delimited -strings. That cannot represent an argument that itself contains `\n` or `\r`. -Node frequently spawns itself as: - -```text -edge -e "" +In the baseline process ABI, argv and envp are represented as newline-delimited +strings. That encoding cannot distinguish between a newline that separates two +arguments and a newline that is part of one argument. Any C program using +`posix_spawn()` or `execv()` with an argument containing `\n` can have that one +argument split into several arguments before the child starts. + +The fix will add and use process syscalls that carry argument vectors as real +pointer/count arrays. Wasmer should reconstruct each argv/envp string from its +own pointer and length, preserving embedded newlines and other bytes. + +Minimal Example: + +```c +#include +#include +#include + +extern char **environ; + +int main(void) { + pid_t pid; + char *argv[] = { + "argv-dump", + "first line\nsecond line", + NULL, + }; + + int err = posix_spawnp(&pid, "argv-dump", NULL, NULL, argv, environ); + if (err != 0) { + printf("posix_spawnp failed: %d\n", err); + return 1; + } + + int status = 0; + waitpid(pid, &status, 0); + return status; +} ``` -Splitting that by line feed turns one argument into many. - -Minimal manifestation: - -```js -const { spawnSync } = require('node:child_process'); +`argv-dump` can be any tiny C helper that prints `argv[1]`. The important +property is that `argv[1]` contains an embedded line feed and must arrive as one +argument. -const script = [ - "console.log('line 1')", - "console.log('line 2')", -].join('\n'); +Callgraph and boundary: -const r = spawnSync(process.execPath, ['-e', script], { encoding: 'utf8' }); -console.log(r.stdout); -``` - -Boundary: +Current problematic path: ```text -Node child_process.spawn() - -> libuv uv_spawn() +C posix_spawnp("argv-dump", ["argv-dump", "first line\nsecond line"]) -> wasix-libc posix_spawn() - -> wasix_32v1.proc_spawn3(argv_ptrs, argv_lens, env_ptrs, env_lens) - -> Wasmer process runner + -> old WASIX proc_spawn/proc_spawn2-style ABI + -> combines argv into one newline-delimited buffer + HERE IS THE PROBLEM: embedded newline is indistinguishable from separator + -> Wasmer process runner parses line-delimited arguments + -> child receives argv = ["argv-dump", "first line", "second line"] + HERE IS THE PROBLEM: child argv shape is corrupted before program starts + +C execv(path, argv_with_newline) + -> wasix-libc execv()/execvpe() + -> old proc_exec/proc_exec3-style ABI + -> same delimiter corruption for replacement process ``` Proposed solution: @@ -42,6 +71,45 @@ Add Wasmer imports that receive argument vectors as pointer/count arrays rather than delimiter-encoded strings. The runtime should reconstruct exact byte strings per argument. +Relevant Wasmer code paths: + +```text +lib/wasix/src/lib.rs + export wasix_32v1.proc_spawn3 / wasix_64v1.proc_spawn3 + export wasix_32v1.proc_exec4 / wasix_64v1.proc_exec4 + +lib/wasix/src/syscalls/wasix/proc_spawn3.rs + proc_spawn3(...) + proc_spawn3_impl(...) + read argv/envp arrays from guest memory as pointer/length pairs + +lib/wasix/src/syscalls/wasix/proc_exec4.rs + proc_exec4(...) + proc_exec4_impl(...) + read replacement argv/envp arrays the same way + +lib/wasix/src/syscalls/wasix/proc_spawn2.rs +lib/wasix/src/syscalls/wasix/proc_exec3.rs + keep legacy newline-delimited wrappers only as compatibility shims +``` + +Proposed callgraph: + +```text +C posix_spawnp("argv-dump", argv) + -> wasix-libc posix_spawn() + -> __wasi_proc_spawn3(name, argv_ptrs, argv_lens, env_ptrs, env_lens, ...) + -> Wasmer proc_spawn3_impl(...) + -> read each argv[i] from its own pointer and length + -> child receives argv[1] = "first line\nsecond line" + +C execv(path, argv) + -> wasix-libc execv()/execvpe() + -> __wasi_proc_exec4(name, argv_ptrs, argv_lens, env_ptrs, env_lens, ...) + -> Wasmer proc_exec4_impl(...) + -> replacement process receives exact argv strings +``` + Sketch: ```witx diff --git a/plans/wasix-plan/wasmer/05_partial_success_for_fd_write.md b/plans/wasix-plan/wasmer/05_partial_success_for_fd_write.md index 61bfe78d0..96a182dca 100644 --- a/plans/wasix-plan/wasmer/05_partial_success_for_fd_write.md +++ b/plans/wasix-plan/wasmer/05_partial_success_for_fd_write.md @@ -2,9 +2,14 @@ Why this is a problem: -POSIX-like write APIs can report partial success. If the first iovec writes and -a later iovec fails, returning total failure loses bytes already accepted by the -kernel/runtime and can confuse stream accounting. +In the baseline runtime, a multi-iovec write can be treated as all-or-nothing. +If the first iovec writes successfully and a later iovec fails, Wasmer can return +the later error for the whole call. That hides bytes already accepted by the fd +and breaks POSIX-style partial write accounting. + +The fix will make `fd_write` preserve partial success. If at least one byte has +already been written, a later iovec error should not erase that success. The +syscall should report the successful byte count to the guest. When it occurs: @@ -12,15 +17,38 @@ When it occurs: - stream and HTTP output accounting; - tests around bytes written and close/error ordering. -Minimal manifestation: +Minimal Example: ```c -struct iovec iov[2] = { - { .iov_base = "ok", .iov_len = 2 }, - { .iov_base = bad_ptr, .iov_len = 4 }, -}; +#include +#include + +int main(void) { + char ok[] = "ok"; + char *bad_ptr = (char *)0x1; + struct iovec iov[2] = { + { .iov_base = ok, .iov_len = 2 }, + { .iov_base = bad_ptr, .iov_len = 4 }, + }; -writev(fd, iov, 2); // should be allowed to return 2 + return writev(1, iov, 2) < 0; +} +``` + +Callgraph and boundary: + +Current problematic path: + +```text +C writev(fd, [valid_iov, invalid_or_failing_iov]) + -> wasix-libc writev() + -> __wasi_fd_write(fd, iovs, iovs_len, &nwritten) + -> Wasmer lib/wasix/src/syscalls/wasi/fd_write.rs fd_write_internal(...) + -> writes first iov successfully + -> later iov fails + HERE IS THE PROBLEM: function can return Err(...) for the whole call + -> guest sees failure and loses the already-written byte count + HERE IS THE PROBLEM: POSIX partial success is hidden ``` Proposed solution: @@ -28,6 +56,33 @@ Proposed solution: If one or more iovecs have already written successfully, return the successful byte count instead of converting the later error into total failure. +Relevant Wasmer code paths: + +```text +lib/wasix/src/syscalls/wasi/fd_write.rs + fd_write(...) + fd_write_internal(...) + track bytes_written across iovs + if an error happens after bytes_written > 0, return Ok(bytes_written) + +lib/wasix/src/syscalls/wasix/sock_send.rs + routes socket send fallback through fd_write_internal for pipe-like fds + +lib/wasix/src/journal/effector/syscalls/fd_write.rs + replay path should preserve the same partial-write semantics +``` + +Proposed callgraph: + +```text +C writev(fd, [valid_iov, invalid_or_failing_iov]) + -> Wasmer fd_write_internal(...) + -> write valid_iov, bytes_written += n + -> later iov returns error + -> because bytes_written > 0, return Ok(bytes_written) + -> guest observes partial successful write +``` + Sketch: ```rust diff --git a/plans/wasix-plan/wasmer/06_future_sendmsg_recvmsg_control_data_and_fd_passing.md b/plans/wasix-plan/wasmer/06_future_sendmsg_recvmsg_control_data_and_fd_passing.md index e11e19559..b4974e73c 100644 --- a/plans/wasix-plan/wasmer/06_future_sendmsg_recvmsg_control_data_and_fd_passing.md +++ b/plans/wasix-plan/wasmer/06_future_sendmsg_recvmsg_control_data_and_fd_passing.md @@ -2,9 +2,17 @@ Why this is a problem: -Plain IPC messages can travel as stream bytes. Passing a socket or server handle -between processes requires ancillary data, normally `sendmsg()` with -`SCM_RIGHTS`. Node cluster shared UDP handles need this. +In the baseline runtime, payload bytes on a socketpair can move, but ancillary +control data is not a real runtime object. POSIX `SCM_RIGHTS` is not just bytes +attached to a message. It means the sending process asks the OS to duplicate or +transfer fd table entries into the receiving process with the correct rights, +lifetime, and close-on-exec behavior. + +If Wasmer accepts `sendmsg()` control data but drops it, the receiver gets a +normal payload byte and no usable handle. If Wasmer returns `ENOTSUP`, the +failure is honest but cluster-style shared handle tests cannot pass. The fix +will make `sock_send_msg` and `sock_recv_msg` carry runtime-owned fd metadata +alongside stream data. When it occurs: @@ -12,28 +20,76 @@ When it occurs: - `test-dgram-bind-shared-ports`; - cluster rows that need a worker to receive a shared handle. -Minimal manifestation: - -```js -// Parent -worker.send({ cmd: 'use this socket' }, udpSocket); - -// Child -process.on('message', (msg, handle) => { - handle.on('message', () => {}); -}); +Minimal Example: + +```c +#include +#include +#include +#include + +int main(void) { + int sv[2]; + socketpair(AF_UNIX, SOCK_STREAM, 0, sv); + + int fd_to_pass = sv[0]; + char byte = 'x'; + char control[CMSG_SPACE(sizeof(int))]; + + struct iovec iov = {.iov_base = &byte, .iov_len = 1}; + struct msghdr msg = {0}; + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + msg.msg_control = control; + msg.msg_controllen = sizeof(control); + + struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); + cmsg->cmsg_level = SOL_SOCKET; + cmsg->cmsg_type = SCM_RIGHTS; + cmsg->cmsg_len = CMSG_LEN(sizeof(int)); + memcpy(CMSG_DATA(cmsg), &fd_to_pass, sizeof(int)); + + sendmsg(sv[0], &msg, 0); + + char out; + char recv_control[CMSG_SPACE(sizeof(int))]; + struct iovec recv_iov = {.iov_base = &out, .iov_len = 1}; + struct msghdr recv_msg = {0}; + recv_msg.msg_iov = &recv_iov; + recv_msg.msg_iovlen = 1; + recv_msg.msg_control = recv_control; + recv_msg.msg_controllen = sizeof(recv_control); + + recvmsg(sv[1], &recv_msg, 0); + struct cmsghdr *received = CMSG_FIRSTHDR(&recv_msg); + printf("received control type: %d\n", received ? received->cmsg_type : -1); +} ``` -Boundary: +Callgraph and boundary: + +Current problematic path: ```text -Node cluster/fork - -> child_process IPC channel on fd 3 - -> libuv uv_write2() - -> sendmsg(..., SCM_RIGHTS) +C socketpair(AF_UNIX, SOCK_STREAM, 0, sv) + -> wasix-libc socketpair() + -> Wasmer creates connected pipe/socketpair-like fds + +C sendmsg(sv[0], payload + SCM_RIGHTS(fd_to_pass)) -> wasix-libc sendmsg() - -> Wasmer sock_send_msg(control) - -> receiver recvmsg(control) + -> __wasi_sock_send_msg(..., control_ptr, control_len) + -> Wasmer sock_send_msg(...) + -> payload path can send normal bytes + -> control path returns ENOTSUP or ignores metadata + HERE IS THE PROBLEM: fd table entry is not duplicated into receiver + +C recvmsg(sv[1], ...) + -> wasix-libc recvmsg() + -> __wasi_sock_recv_msg(..., ro_control, ro_control_len) + -> Wasmer sock_recv_msg(...) + -> payload byte may arrive + -> no queued rights metadata exists + HERE IS THE PROBLEM: receiver has no fd to install into its fd table ``` Proposed solution: @@ -47,6 +103,44 @@ Implement runtime-owned handle passing: - serialize received handles into the receiver fd table during `sock_recv_msg`; - report truncation if the receiver's control buffer is too small. +Relevant Wasmer code paths: + +```text +lib/wasix/src/syscalls/wasix/sock_send_msg.rs + parse incoming control buffer + convert SOCKET/RIGHTS control entries into runtime fd references + +lib/wasix/src/syscalls/wasix/sock_recv_msg.rs + deliver queued control entries + allocate receiver-side fd numbers + write cmsghdr-compatible control bytes back to guest memory + +lib/wasix/src/fs/mod.rs + fd table duplication, rights, cloexec, and target fd allocation + +lib/wasix/src/net/socket.rs + socketpair / DuplexPipe / pipe-backed IPC transport state +``` + +Proposed callgraph: + +```text +C sendmsg(sv[0], payload + SCM_RIGHTS(fd_to_pass)) + -> wasix-libc sendmsg() + -> Wasmer sock_send_msg(...) + -> parse control message as fd_to_pass + -> duplicate sender fd entry and rights into queued control metadata + -> send payload byte plus queued control metadata through socketpair state + +C recvmsg(sv[1], ...) + -> wasix-libc recvmsg() + -> Wasmer sock_recv_msg(...) + -> receive payload byte + -> allocate receiver fd for duplicated entry + -> write SCM_RIGHTS control record containing receiver fd number + -> C caller receives payload and a usable fd +``` + Do not silently drop control data. Until implemented, returning `ENOTSUP` is more honest than pretending descriptor passing worked. From 37a4862008eea4ea1cb949d8e627b5f86fbb601f Mon Sep 17 00:00:00 2001 From: Sadhbh Code Date: Mon, 15 Jun 2026 11:00:01 +0100 Subject: [PATCH 25/65] Update napi branch --- napi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/napi b/napi index 21f22e3cc..0a84413ba 160000 --- a/napi +++ b/napi @@ -1 +1 @@ -Subproject commit 21f22e3cc3b90a2c453d2e4115e0db42c5d8f68a +Subproject commit 0a84413ba8c8f877f608442a25ba749b76fc027d From bc93737c3867245180ea12080ed09688749cc242 Mon Sep 17 00:00:00 2001 From: Sadhbh Code Date: Mon, 15 Jun 2026 11:27:51 +0100 Subject: [PATCH 26/65] Updated wasix test plan --- ..._lifecycle_and_address_family_selection.md | 6 +++-- ...te_files_instead_of_hard_coding_answers.md | 6 +++-- ...orkarounds_after_lower_layer_plans_land.md | 8 +++--- ...sl_error_mapping_and_version_capability.md | 6 +++-- ...5_runner_determinism_and_workflow_setup.md | 16 ++++++------ .../01_use_posix_spawn_on_wasix.md | 4 ++- ...nect_and_multicast_option_compatibility.md | 4 ++- .../03_tcp_keepalive_timing_options.md | 4 ++- ...ured_clone_serdes_for_sharedarraybuffer.md | 9 +++++-- .../napi/02_quickjs_napi_callsite_frames.md | 6 +++-- ...ack_limit_should_match_non_wasi_quickjs.md | 6 +++-- ...pe_flags_sock_nonblock_and_sock_cloexec.md | 8 ++---- ..._boolean_socket_option_pointer_handling.md | 8 ++---- .../wasix-libc/03_getsockopt_so_error.md | 14 +++-------- ...posix_st_mode_for_files_and_directories.md | 8 ++---- .../wasix-libc/05_loopback_reverse_lookup.md | 8 ++---- ..._datagram_receive_readiness_and_backlog.md | 17 ++++++------- ...nected_udp_send_peer_state_and_emsgsize.md | 10 +++----- .../wasmer/03_last_socket_error_so_error.md | 14 +++-------- ...oc_spawn3_proc_exec4_for_real_argv_envp.md | 25 ++++++------------- .../wasmer/05_partial_success_for_fd_write.md | 8 ++---- ...msg_recvmsg_control_data_and_fd_passing.md | 10 +++----- 22 files changed, 90 insertions(+), 115 deletions(-) diff --git a/plans/wasix-plan/edgejs/01_c_ares_request_lifecycle_and_address_family_selection.md b/plans/wasix-plan/edgejs/01_c_ares_request_lifecycle_and_address_family_selection.md index 548b14128..a9925cb49 100644 --- a/plans/wasix-plan/edgejs/01_c_ares_request_lifecycle_and_address_family_selection.md +++ b/plans/wasix-plan/edgejs/01_c_ares_request_lifecycle_and_address_family_selection.md @@ -127,5 +127,7 @@ and preserve the requested address family when constructing c-ares work. ## Proposed Solution References -- edgejs `929a9151` `c-ares requests are now tracked per channel, cancellation completes still-active requests with ECANCELLED, reverse lookup includes h_name, and loopback reverse/nameinfo returns localhost` -- edgejs `64faa6ca` `Respect address family (IPv4 or IPv6) when asked GetAddrInfo` +### [wasmerio/edgejs#91: [WIP] Node tests using Edgejs WASIX QuickJS](https://github.com/wasmerio/edgejs/pull/91) + +- edgejs [929a9151](https://github.com/wasmerio/edgejs/commit/929a9151c336b5862839d7b3d3e2bec219f852ca) c-ares requests are now tracked per channel, cancellation completes still-active requests with ECANCELLED, reverse lookup includes h_name, and loopback reverse/nameinfo returns localhost +- edgejs [64faa6ca](https://github.com/wasmerio/edgejs/commit/64faa6caba3648b7393c69c9a339a4b90f2a697e) Respect address family (IPv4 or IPv6) when asked GetAddrInfo diff --git a/plans/wasix-plan/edgejs/02_mount_resolver_and_certificate_files_instead_of_hard_coding_answers.md b/plans/wasix-plan/edgejs/02_mount_resolver_and_certificate_files_instead_of_hard_coding_answers.md index 84aeddd4b..75398cef5 100644 --- a/plans/wasix-plan/edgejs/02_mount_resolver_and_certificate_files_instead_of_hard_coding_answers.md +++ b/plans/wasix-plan/edgejs/02_mount_resolver_and_certificate_files_instead_of_hard_coding_answers.md @@ -115,5 +115,7 @@ logic. ## Proposed Solution References -- edgejs `dc9a0465` `Added /etc/hosts, removed stream type normalization` -- edgejs `38e01f79` `Napi/Qjs: SharedArrayBuffer. Ssl certs in wasix tests.` +### [wasmerio/edgejs#91: [WIP] Node tests using Edgejs WASIX QuickJS](https://github.com/wasmerio/edgejs/pull/91) + +- edgejs [dc9a0465](https://github.com/wasmerio/edgejs/commit/dc9a046509ff36b4f1e952967528480a5873e4b2) Added /etc/hosts, removed stream type normalization +- edgejs [38e01f79](https://github.com/wasmerio/edgejs/commit/38e01f79c15636badbab17faf6c31eef7d16abc6) Napi/Qjs: SharedArrayBuffer. Ssl certs in wasix tests. diff --git a/plans/wasix-plan/edgejs/03_remove_wasix_only_guest_workarounds_after_lower_layer_plans_land.md b/plans/wasix-plan/edgejs/03_remove_wasix_only_guest_workarounds_after_lower_layer_plans_land.md index 22663a6e0..169728826 100644 --- a/plans/wasix-plan/edgejs/03_remove_wasix_only_guest_workarounds_after_lower_layer_plans_land.md +++ b/plans/wasix-plan/edgejs/03_remove_wasix_only_guest_workarounds_after_lower_layer_plans_land.md @@ -122,6 +122,8 @@ branch. ## Proposed Solution References -- edgejs `f1999f45` `Removed hacks: loopback, and spawn` -- edgejs `27118329` `Fixes around edge process wrap, plus w/a for edge -e eval` -- edgejs `61ba9604` `various fixes` +### [wasmerio/edgejs#91: [WIP] Node tests using Edgejs WASIX QuickJS](https://github.com/wasmerio/edgejs/pull/91) + +- edgejs [f1999f45](https://github.com/wasmerio/edgejs/commit/f1999f45d43453d508db45b3b52e4115983e26a8) Removed hacks: loopback, and spawn +- edgejs [27118329](https://github.com/wasmerio/edgejs/commit/27118329f47b9876c3f415bf42669f9cc4a6568b) Fixes around edge process wrap, plus w/a for edge -e eval +- edgejs [61ba9604](https://github.com/wasmerio/edgejs/commit/61ba9604327a7326a555c2d537d7214421c60a9c) various fixes diff --git a/plans/wasix-plan/edgejs/04_openssl_error_mapping_and_version_capability.md b/plans/wasix-plan/edgejs/04_openssl_error_mapping_and_version_capability.md index bc6cd871f..e66da29c5 100644 --- a/plans/wasix-plan/edgejs/04_openssl_error_mapping_and_version_capability.md +++ b/plans/wasix-plan/edgejs/04_openssl_error_mapping_and_version_capability.md @@ -105,5 +105,7 @@ which OpenSSL stack entry should become a Node.js error. ## Proposed Solution References -- edgejs `5254f803` `OpenSSL update to 3.5` -- edgejs `b9ef182f` `Fixes for OpenSSL error code mapping` +### [wasmerio/edgejs#91: [WIP] Node tests using Edgejs WASIX QuickJS](https://github.com/wasmerio/edgejs/pull/91) + +- edgejs [5254f803](https://github.com/wasmerio/edgejs/commit/5254f803ff93200d79231d87501f813eb1b35147) OpenSSL update to 3.5 +- edgejs [b9ef182f](https://github.com/wasmerio/edgejs/commit/b9ef182f2322eb0e111f40427e4685852ec6a241) Fixes for OpenSSL error code mapping diff --git a/plans/wasix-plan/edgejs/05_runner_determinism_and_workflow_setup.md b/plans/wasix-plan/edgejs/05_runner_determinism_and_workflow_setup.md index 827e19798..ff4d48a2b 100644 --- a/plans/wasix-plan/edgejs/05_runner_determinism_and_workflow_setup.md +++ b/plans/wasix-plan/edgejs/05_runner_determinism_and_workflow_setup.md @@ -96,10 +96,12 @@ This is an EdgeJS-owned test-infra plan. It should not change runtime semantics. ## Proposed Solution References -- edgejs `f87e6848` `Added note test runner for wasix quickjs` -- edgejs `2baeceea` `Added node test runner wasix quickjs to gh workflow` -- edgejs `352c55f3` `Set wasmer version to 7.2.0-alpha.3 for wasix tests` -- edgejs `c63d7771` `Install wasmer 7.2.0-alpha.3 using curl and shell script. Disable (temporarily other workflows).` -- edgejs `fbbb21f7` `Updated sysroot=v2026-05-26.1 wasixcc=v0.4.3` -- edgejs `566e59a5` `Force llvm runtime` -- edgejs `9aefbbe2` `Isolate mapped host directories for wasix node test runner` +### [wasmerio/edgejs#91: [WIP] Node tests using Edgejs WASIX QuickJS](https://github.com/wasmerio/edgejs/pull/91) + +- edgejs [f87e6848](https://github.com/wasmerio/edgejs/commit/f87e68487996f12d447974d0cefb85c3e956e885) Added note test runner for wasix quickjs +- edgejs [2baeceea](https://github.com/wasmerio/edgejs/commit/2baeceeaba641eb8e5ad61a88c1caf5b75a24547) Added node test runner wasix quickjs to gh workflow +- edgejs [352c55f3](https://github.com/wasmerio/edgejs/commit/352c55f3cf2d2b1083c48eb4cf190b3b43859ce0) Set wasmer version to 7.2.0-alpha.3 for wasix tests +- edgejs [c63d7771](https://github.com/wasmerio/edgejs/commit/c63d77715b7f0d96ac1ef027cfa885ffcae229e2) Install wasmer 7.2.0-alpha.3 using curl and shell script. Disable (temporarily other workflows). +- edgejs [fbbb21f7](https://github.com/wasmerio/edgejs/commit/fbbb21f7500bfa0f80cd84a49e63e579c929bebe) Updated sysroot=v2026-05-26.1 wasixcc=v0.4.3 +- edgejs [566e59a5](https://github.com/wasmerio/edgejs/commit/566e59a5b5dc451c4970dcc095ed80d6ed9e3120) Force llvm runtime +- edgejs [9aefbbe2](https://github.com/wasmerio/edgejs/commit/9aefbbe29753d32a43dcd3b11b9cd8dbbcb99b30) Isolate mapped host directories for wasix node test runner diff --git a/plans/wasix-plan/libuv-wasix/01_use_posix_spawn_on_wasix.md b/plans/wasix-plan/libuv-wasix/01_use_posix_spawn_on_wasix.md index 04f185893..43133b943 100644 --- a/plans/wasix-plan/libuv-wasix/01_use_posix_spawn_on_wasix.md +++ b/plans/wasix-plan/libuv-wasix/01_use_posix_spawn_on_wasix.md @@ -82,4 +82,6 @@ stdio container into `posix_spawn_file_actions_*` calls plus `posix_spawnp()`. ## Proposed Solution References -- libuv-wasix `ba06698b` `libuv-wasix: use WASIX posix_spawn/proc_join for child processes` +### No PR group + +- libuv-wasix [ba06698b](https://github.com/Anodized-Titanium/libuv-wasix/commit/ba06698ba3f3b508a7a4eeacb23e0c911ebf4576) libuv-wasix: use WASIX posix_spawn/proc_join for child processes diff --git a/plans/wasix-plan/libuv-wasix/02_udp_disconnect_and_multicast_option_compatibility.md b/plans/wasix-plan/libuv-wasix/02_udp_disconnect_and_multicast_option_compatibility.md index d7d57d9e3..de1099c5c 100644 --- a/plans/wasix-plan/libuv-wasix/02_udp_disconnect_and_multicast_option_compatibility.md +++ b/plans/wasix-plan/libuv-wasix/02_udp_disconnect_and_multicast_option_compatibility.md @@ -92,4 +92,6 @@ multicast implementation remains a Wasmer task. ## Proposed Solution References -- libuv-wasix `0ae770b9` `multicast TTL/loop/interface shims and UDP disconnect handling` +### No PR group + +- libuv-wasix [0ae770b9](https://github.com/Anodized-Titanium/libuv-wasix/commit/0ae770b9daae35d97d3c9a2693a5b22362124f12) multicast TTL/loop/interface shims and UDP disconnect handling diff --git a/plans/wasix-plan/libuv-wasix/03_tcp_keepalive_timing_options.md b/plans/wasix-plan/libuv-wasix/03_tcp_keepalive_timing_options.md index 7d8c280fb..1db0eed48 100644 --- a/plans/wasix-plan/libuv-wasix/03_tcp_keepalive_timing_options.md +++ b/plans/wasix-plan/libuv-wasix/03_tcp_keepalive_timing_options.md @@ -75,4 +75,6 @@ claiming that WASIX already implements every TCP timing option. ## Proposed Solution References -- edgejs `9fa61f18` `UV keep alive fix (disable unsupported options)` +### [wasmerio/edgejs#91: [WIP] Node tests using Edgejs WASIX QuickJS](https://github.com/wasmerio/edgejs/pull/91) + +- edgejs [9fa61f18](https://github.com/wasmerio/edgejs/commit/9fa61f1888f34c0785f54da8dd99ae193e536439) UV keep alive fix (disable unsupported options) diff --git a/plans/wasix-plan/napi/01_quickjs_structured_clone_serdes_for_sharedarraybuffer.md b/plans/wasix-plan/napi/01_quickjs_structured_clone_serdes_for_sharedarraybuffer.md index 7510475ad..d1629578a 100644 --- a/plans/wasix-plan/napi/01_quickjs_structured_clone_serdes_for_sharedarraybuffer.md +++ b/plans/wasix-plan/napi/01_quickjs_structured_clone_serdes_for_sharedarraybuffer.md @@ -88,5 +88,10 @@ bridges QuickJS SAB lifetime with Node/N-API ownership. ## Proposed Solution References -- napi `21f22e3` `QJS: Serdes for SharedArrayBuffer` -- edgejs `38e01f79` `Napi/Qjs: SharedArrayBuffer. Ssl certs in wasix tests.` +### [wasmerio/edgejs#91: [WIP] Node tests using Edgejs WASIX QuickJS](https://github.com/wasmerio/edgejs/pull/91) + +- edgejs [38e01f79](https://github.com/wasmerio/edgejs/commit/38e01f79c15636badbab17faf6c31eef7d16abc6) Napi/Qjs: SharedArrayBuffer. Ssl certs in wasix tests. + +### No PR group + +- napi [21f22e3](https://github.com/wasmerio/napi/commit/21f22e3cc3b90a2c453d2e4115e0db42c5d8f68a) QJS: Serdes for SharedArrayBuffer diff --git a/plans/wasix-plan/napi/02_quickjs_napi_callsite_frames.md b/plans/wasix-plan/napi/02_quickjs_napi_callsite_frames.md index 669f17437..f6ac581bc 100644 --- a/plans/wasix-plan/napi/02_quickjs_napi_callsite_frames.md +++ b/plans/wasix-plan/napi/02_quickjs_napi_callsite_frames.md @@ -87,5 +87,7 @@ EdgeJS feature that happens to need caller metadata. ## Proposed Solution References -- quickjs `41b00d4` `Added callsites into QuickJS` -- napi `5a5c3b2` `Improved quickjs callsites similar to v8` +### No PR group + +- quickjs [41b00d4](https://github.com/wasmerio/quickjs/commit/41b00d4cc34cf79188cd9255f050e95ea1a2e9d6) Added callsites into QuickJS +- napi [5a5c3b2](https://github.com/wasmerio/napi/commit/5a5c3b283aa9b37e53898543fb33ee0255a43e1e) Improved quickjs callsites similar to v8 diff --git a/plans/wasix-plan/quickjs/01_wasi_stack_limit_should_match_non_wasi_quickjs.md b/plans/wasix-plan/quickjs/01_wasi_stack_limit_should_match_non_wasi_quickjs.md index d590c23bb..814201fa5 100644 --- a/plans/wasix-plan/quickjs/01_wasi_stack_limit_should_match_non_wasi_quickjs.md +++ b/plans/wasix-plan/quickjs/01_wasi_stack_limit_should_match_non_wasi_quickjs.md @@ -83,5 +83,7 @@ shape of JavaScript stack overflow errors. ## Proposed Solution References -- quickjs `9a59f17` `Set WASI stack limit same as non-WASI` -- quickjs `cec4427` `Improved stack setter` +### No PR group + +- quickjs [9a59f17](https://github.com/wasmerio/quickjs/commit/9a59f17a026ebc3b6932afa886c21ebf02689a2e) Set WASI stack limit same as non-WASI +- quickjs [cec4427](https://github.com/wasmerio/quickjs/commit/cec44274d0b0abb9670e978a32ed4b4c2dd3b0bd) Improved stack setter diff --git a/plans/wasix-plan/wasix-libc/01_socket_type_flags_sock_nonblock_and_sock_cloexec.md b/plans/wasix-plan/wasix-libc/01_socket_type_flags_sock_nonblock_and_sock_cloexec.md index 9ac25c0e0..bb187286d 100644 --- a/plans/wasix-plan/wasix-libc/01_socket_type_flags_sock_nonblock_and_sock_cloexec.md +++ b/plans/wasix-plan/wasix-libc/01_socket_type_flags_sock_nonblock_and_sock_cloexec.md @@ -94,10 +94,6 @@ int socket(int domain, int type, int protocol) { ## Proposed Solution References -Proposed solution can be found in: +### [wasix-org/wasix-libc#116: Fix socket options and last error](https://github.com/wasix-org/wasix-libc/pull/116) -**Reference commits:** - -```text -wasix-libc c0853db Open sockets with nonblock|cloexec flags correctly -``` +- wasix-libc [c0853db](https://github.com/Anodized-Titanium/wasix-libc/commit/c0853db552fc0b85a23cdbe35a4f1d5b37c3cb8a) Open sockets with nonblock|cloexec flags correctly diff --git a/plans/wasix-plan/wasix-libc/02_boolean_socket_option_pointer_handling.md b/plans/wasix-plan/wasix-libc/02_boolean_socket_option_pointer_handling.md index f9eb775d1..d80d54e9d 100644 --- a/plans/wasix-plan/wasix-libc/02_boolean_socket_option_pointer_handling.md +++ b/plans/wasix-plan/wasix-libc/02_boolean_socket_option_pointer_handling.md @@ -84,10 +84,6 @@ return __wasi_sock_set_opt_flag(fd, level, optname, enabled != 0); ## Proposed Solution References -Proposed solution can be found in: +### [wasix-org/wasix-libc#116: Fix socket options and last error](https://github.com/wasix-org/wasix-libc/pull/116) -**Reference commits:** - -```text -wasix-libc 17e686c Socket option incorrectly deferenced pointer -``` +- wasix-libc [17e686c](https://github.com/Anodized-Titanium/wasix-libc/commit/17e686c6a1cd6f11b3085aa23a42c0263fe99de5) Socket option incorrectly deferenced pointer diff --git a/plans/wasix-plan/wasix-libc/03_getsockopt_so_error.md b/plans/wasix-plan/wasix-libc/03_getsockopt_so_error.md index 5e03d9732..1116300cd 100644 --- a/plans/wasix-plan/wasix-libc/03_getsockopt_so_error.md +++ b/plans/wasix-plan/wasix-libc/03_getsockopt_so_error.md @@ -67,16 +67,10 @@ C getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &len) ## Proposed Solution References -Proposed solution can be found in: +### [wasix-org/wasix-libc#116: Fix socket options and last error](https://github.com/wasix-org/wasix-libc/pull/116) -**Reference commits:** +- wasix-libc [13626ca](https://github.com/Anodized-Titanium/wasix-libc/commit/13626cacc589cd284f97bd724715fb0184e2bc38) Last socket error -```text -wasix-libc 13626ca Last socket error -``` - -**Cross-project pair:** +### [wasmerio/wasmer#6685: Udp datagram receive and last err](https://github.com/wasmerio/wasmer/pull/6685) -```text -wasmer 0780b11bb2a Last socket error -``` +- wasmer [0780b11bb2a](https://github.com/Anodized-Titanium/wasmer/commit/0780b11bb2abc4bfd532695c5887b215f6efbed7) Last socket error diff --git a/plans/wasix-plan/wasix-libc/04_posix_st_mode_for_files_and_directories.md b/plans/wasix-plan/wasix-libc/04_posix_st_mode_for_files_and_directories.md index 59798d0c2..730dd448b 100644 --- a/plans/wasix-plan/wasix-libc/04_posix_st_mode_for_files_and_directories.md +++ b/plans/wasix-plan/wasix-libc/04_posix_st_mode_for_files_and_directories.md @@ -107,10 +107,6 @@ st->st_mode = mode; ## Proposed Solution References -Proposed solution can be found in: +### No PR group -**Reference commits:** - -```text -wasix-libc 57b1083 Fix dir and file flags -``` +- wasix-libc [57b1083](https://github.com/Anodized-Titanium/wasix-libc/commit/57b108379c93b2847d5d8b373067a54f0f083293) Fix dir and file flags diff --git a/plans/wasix-plan/wasix-libc/05_loopback_reverse_lookup.md b/plans/wasix-plan/wasix-libc/05_loopback_reverse_lookup.md index 834d6c0f7..79a6a26ed 100644 --- a/plans/wasix-plan/wasix-libc/05_loopback_reverse_lookup.md +++ b/plans/wasix-plan/wasix-libc/05_loopback_reverse_lookup.md @@ -100,10 +100,6 @@ case AF_INET6: ## Proposed Solution References -Proposed solution can be found in: +### No PR group -**Reference commits:** - -```text -wasix-libc f157cd9 Reverse loopback -``` +- wasix-libc [f157cd9](https://github.com/Anodized-Titanium/wasix-libc/commit/f157cd9aaa3ee6c3e6aafa4b2afb6192b543783b) Reverse loopback diff --git a/plans/wasix-plan/wasmer/01_udp_datagram_receive_readiness_and_backlog.md b/plans/wasix-plan/wasmer/01_udp_datagram_receive_readiness_and_backlog.md index 305e3dcfd..c1bc709c4 100644 --- a/plans/wasix-plan/wasmer/01_udp_datagram_receive_readiness_and_backlog.md +++ b/plans/wasix-plan/wasmer/01_udp_datagram_receive_readiness_and_backlog.md @@ -189,14 +189,13 @@ guest buffer. ## Proposed Solution References -Proposed solution can be found in: +### [wasmerio/wasmer#6685: Udp datagram receive and last err](https://github.com/wasmerio/wasmer/pull/6685) -**Reference commits:** +- wasmer [c56897f3bec](https://github.com/Anodized-Titanium/wasmer/commit/c56897f3beced2c9c4861137bf81f32320a14b7c) Fixed UDP socket dgram receive +- wasmer [65e0eb571ef](https://github.com/Anodized-Titanium/wasmer/commit/65e0eb571ef4fde1aee765fae2955956dcbd1d8f) Peek UDP packets correctly +- wasmer [66552d27be9](https://github.com/Anodized-Titanium/wasmer/commit/66552d27be9a7efc7962aab22bd60375476c07bb) Fix merge conflict mistake +- wasmer [8e92612e917](https://github.com/Anodized-Titanium/wasmer/commit/8e92612e917cb9745f21aab2634dd7687c229d33) Merge branch 'main' into udp-datagram-receive-and-last-err -```text -wasmer c56897f3bec Fixed UDP socket dgram receive -wasmer 65e0eb571ef Peek UDP packets correctly -wasmer 66552d27be9 Fix merge conflict mistake -wasmer 8e92612e917 Merge branch 'main' into udp-datagram-receive-and-last-err -wasmer bee1e0ab8ae Merge branch 'udp-datagram-receive-and-last-err' into tmp-work-4 -``` +### No PR group + +- wasmer [bee1e0ab8ae](https://github.com/Anodized-Titanium/wasmer/commit/bee1e0ab8aef47c6794d89fd5263f9301cc5837b) Merge branch 'udp-datagram-receive-and-last-err' into tmp-work-4 diff --git a/plans/wasix-plan/wasmer/02_connected_udp_send_peer_state_and_emsgsize.md b/plans/wasix-plan/wasmer/02_connected_udp_send_peer_state_and_emsgsize.md index a9969fe92..c442cd8b9 100644 --- a/plans/wasix-plan/wasmer/02_connected_udp_send_peer_state_and_emsgsize.md +++ b/plans/wasix-plan/wasmer/02_connected_udp_send_peer_state_and_emsgsize.md @@ -181,11 +181,7 @@ fn udp_connected_send(iovs: &[IoSlice<'_>], peer: SocketAddr) -> Result Errno { ## Proposed Solution References -Proposed solution can be found in: +### [wasix-org/wasix-libc#116: Fix socket options and last error](https://github.com/wasix-org/wasix-libc/pull/116) -**Reference commits:** +- wasix-libc [13626ca](https://github.com/Anodized-Titanium/wasix-libc/commit/13626cacc589cd284f97bd724715fb0184e2bc38) Last socket error -```text -wasmer 0780b11bb2a Last socket error -``` - -**Cross-project pair:** +### [wasmerio/wasmer#6685: Udp datagram receive and last err](https://github.com/wasmerio/wasmer/pull/6685) -```text -wasix-libc 13626ca Last socket error -``` +- wasmer [0780b11bb2a](https://github.com/Anodized-Titanium/wasmer/commit/0780b11bb2abc4bfd532695c5887b215f6efbed7) Last socket error diff --git a/plans/wasix-plan/wasmer/04_proc_spawn3_proc_exec4_for_real_argv_envp.md b/plans/wasix-plan/wasmer/04_proc_spawn3_proc_exec4_for_real_argv_envp.md index b55d382ef..d8a982803 100644 --- a/plans/wasix-plan/wasmer/04_proc_spawn3_proc_exec4_for_real_argv_envp.md +++ b/plans/wasix-plan/wasmer/04_proc_spawn3_proc_exec4_for_real_argv_envp.md @@ -132,20 +132,11 @@ spawn_process(path, argv, envp, file_actions) ## Proposed Solution References -Proposed solution can be found in: - -**Reference commits:** - -```text -wasmer d80c3d8c980 Add proc_spawn3 and proc_exec4... -wasmer b5880b5268b apply review comments -wasmer e9d1478b914 fix tests -wasmer bda2ead2504 Merge branch 'feat/wasix-proc-spawn3' into tmp-work-4 -``` - -**Cross-project pair:** - -```text -wasix-libc proc_spawn3/proc_exec4 lowering -libuv-wasix ba06698b libuv-wasix: use WASIX posix_spawn/proc_join... -``` +### No PR group + +- wasmer [d80c3d8c980](https://github.com/Anodized-Titanium/wasmer/commit/d80c3d8c98065562c845c2ec8acfe0668798a166) Add proc_spawn3 and proc_exec4... +- wasmer [b5880b5268b](https://github.com/Anodized-Titanium/wasmer/commit/b5880b5268b74aefc5b4295767286976d83b0deb) apply review comments +- wasmer [e9d1478b914](https://github.com/Anodized-Titanium/wasmer/commit/e9d1478b914c224051541d35281ff0a0638fab21) fix tests +- wasmer [bda2ead2504](https://github.com/Anodized-Titanium/wasmer/commit/bda2ead2504246057a4d91d3dc5b7b071e7ed9bd) Merge branch 'feat/wasix-proc-spawn3' into tmp-work-4 +- wasix-libc proc_spawn3/proc_exec4 lowering +- libuv-wasix [ba06698b](https://github.com/Anodized-Titanium/libuv-wasix/commit/ba06698ba3f3b508a7a4eeacb23e0c911ebf4576) libuv-wasix: use WASIX posix_spawn/proc_join... diff --git a/plans/wasix-plan/wasmer/05_partial_success_for_fd_write.md b/plans/wasix-plan/wasmer/05_partial_success_for_fd_write.md index 96a182dca..1a3e0493c 100644 --- a/plans/wasix-plan/wasmer/05_partial_success_for_fd_write.md +++ b/plans/wasix-plan/wasmer/05_partial_success_for_fd_write.md @@ -99,10 +99,6 @@ Ok(written) ## Proposed Solution References -Proposed solution can be found in: +### [wasmerio/wasmer#6685: Udp datagram receive and last err](https://github.com/wasmerio/wasmer/pull/6685) -**Reference commits:** - -```text -wasmer 945aac9b18f fd_write should not fail it there was at least one success -``` +- wasmer [945aac9b18f](https://github.com/Anodized-Titanium/wasmer/commit/945aac9b18fafbbee9b5605e6f57211dd94afaf8) fd_write should not fail it there was at least one success diff --git a/plans/wasix-plan/wasmer/06_future_sendmsg_recvmsg_control_data_and_fd_passing.md b/plans/wasix-plan/wasmer/06_future_sendmsg_recvmsg_control_data_and_fd_passing.md index b4974e73c..54000e411 100644 --- a/plans/wasix-plan/wasmer/06_future_sendmsg_recvmsg_control_data_and_fd_passing.md +++ b/plans/wasix-plan/wasmer/06_future_sendmsg_recvmsg_control_data_and_fd_passing.md @@ -146,11 +146,7 @@ more honest than pretending descriptor passing worked. ## Proposed Solution References -Proposed solution can be found in: +### No PR group -**Reference status:** - -```text -WITX/libc payload ABI exists as plan/input. -Phase 2 SCM_RIGHTS/handle passing remains a Wasmer runtime task. -``` +- WITX/libc payload ABI exists as plan/input. +- Phase 2 SCM_RIGHTS/handle passing remains a Wasmer runtime task. From 1b0cd4c000d064b70d3e463c965db5a6a53f43b8 Mon Sep 17 00:00:00 2001 From: Sadhbh Code Date: Mon, 15 Jun 2026 11:29:46 +0100 Subject: [PATCH 27/65] Small update --- .../009_node_test_failures_analysis.md | 45 ++++ .../napi/018_imported_napi_wrap_finalizer.md | 150 ++++++++++++ .../019_imported_napi_repl_input_stall.md | 228 ++++++++++++++++++ 3 files changed, 423 insertions(+) create mode 100644 plans/quickjs-wasm/development/troubleshooting/node-compat/napi/018_imported_napi_wrap_finalizer.md create mode 100644 plans/quickjs-wasm/development/troubleshooting/node-compat/napi/019_imported_napi_repl_input_stall.md diff --git a/plans/quickjs-wasm/development/009_node_test_failures_analysis.md b/plans/quickjs-wasm/development/009_node_test_failures_analysis.md index 68bd54418..d7368a72d 100644 --- a/plans/quickjs-wasm/development/009_node_test_failures_analysis.md +++ b/plans/quickjs-wasm/development/009_node_test_failures_analysis.md @@ -922,3 +922,48 @@ full quickjs category set total=1779 passed=1740 failed=39 That matches the macOS-sized residual failure set and removes the Linux-only allocator-abort wave from the downloaded CI logs. + +## 2026-06-05 WASIX Node Test Temp Isolation + +A later full local `make test-wasix-quickjs-only` run showed a repeated block +failure: + +```text +EEXIST: file already exists, mkdir '/workspace/test/.tmp.0' +``` + +This was a harness isolation issue, not independent HTTP, HTTP/2, TLS, zlib, or +stream subsystem breakage. `test/common/tmpdir.js` names the shared test temp +directory from `TEST_SERIAL_ID || TEST_THREAD_ID || '0'`. The Python Node test +harness sets per-test serial IDs, but the WASIX runner only forwarded `HOME` +and `NODE_TEST_DIR` through `wasmer run`, so guest tests fell back to `.tmp.0`. + +`scripts/edge-wasix-node-runner.sh` now: + +- forwards `TEST_SERIAL_ID` into the guest; +- synthesizes `wasix--` for direct runner invocations without a + harness-provided serial; +- sets `NODE_TEST_DIR=/tmp/edgejs-node-test`; +- mounts that temp root separately from the source tree; +- mounts a temporary empty root at `/workspace` and then mounts individual + source directories under it instead of mounting the whole checkout as + `/workspace`. + +The default individual source mounts are configurable through +`WASIX_EDGEJS_WORKSPACE_DIRS` and currently default to +`test,lib,deps,node,assets`. + +Focused verification: + +```sh +bash -n /Users/sadhbh/src/dev/edgejs/scripts/edge-wasix-node-runner.sh +/Users/sadhbh/src/dev/edgejs/scripts/edge-wasix-node-runner.sh \ + -e "const tmpdir=require('/workspace/test/common/tmpdir.js'); tmpdir.refresh(); console.log(tmpdir.path);" +/Users/sadhbh/src/dev/edgejs/scripts/edge-wasix-node-runner.sh \ + /Users/sadhbh/src/dev/edgejs/test/parallel/test-url-format.js +``` + +The tmpdir probe resolved under +`/tmp/edgejs-node-test/.tmp.wasix-...`, and two concurrent direct probes used +different pid-suffixed temp directories. `parallel/test-url-format.js` passed +through the WASIX runner. diff --git a/plans/quickjs-wasm/development/troubleshooting/node-compat/napi/018_imported_napi_wrap_finalizer.md b/plans/quickjs-wasm/development/troubleshooting/node-compat/napi/018_imported_napi_wrap_finalizer.md new file mode 100644 index 000000000..13b7f6982 --- /dev/null +++ b/plans/quickjs-wasm/development/troubleshooting/node-compat/napi/018_imported_napi_wrap_finalizer.md @@ -0,0 +1,150 @@ +# Imported N-API wrap finalizer bridge + +| | | Remarks | +| --- | --- | --- | +| **Status** | 🟠 | Startup fix works with rebuilt source Wasmer; signal delivery and true guest finalizer invocation remain follow-up work. | +| **Severity** | High | Blocks the imported N-API root package before the REPL can start. | + +## Failure + +Running the root V8/imports WASIX package with installed Wasmer reaches Node +bootstrap and fails when the REPL logs its greeting: + +```sh +wasmer run --experimental-napi -l . +``` + +Observed failure: + +```text +TypeError: Illegal invocation + at process.startListeningIfSignal (node:internal/process/signal:28:10) + at process.getStdout [as stdout] (node:internal/bootstrap/switches/is_main_thread:159:13) + at console.log (node:internal/console/constructor:416:26) + at node:internal/main/repl:38:13 +``` + +Running the source Wasmer binary from `~/src/dev/wasmer` currently fails earlier: + +```text +Error while importing "napi_extension_wasmer_v0"."unofficial_napi_create_env": unknown import. +``` + +That binary accepts `--experimental-napi`, but the observed behavior is +consistent with a build without the `napi-v8` feature, so it never installs the +N-API runtime hook that provides the extension imports. + +## Diagnosis + +The JS failure is in Edge's `SignalWrap` bootstrap path: + +1. `process.stdout` detects TTY output and registers `SIGWINCH`. +2. `internalBinding('signal_wrap').Signal` constructs a native `SignalWrap`. +3. `SignalCtor(...)` calls `napi_wrap(env, self, wrap, SignalFinalize, ..., &wrapper_ref)`. +4. The imported N-API bridge receives the non-null guest finalizer pointer but + discards it. +5. `snapi_bridge_wrap(...)` calls host V8 `napi_wrap(...)` with + `finalize_cb == nullptr` and `result != nullptr`. +6. The V8 backend rejects that combination with `napi_invalid_arg`. +7. `SignalCtor(...)` currently does not check the status, so the later + `SignalStart(...)` cannot unwrap `this` and throws `Illegal invocation`. + +The bridge also drops guest finalizers for `napi_add_finalizer(...)`, which is a +related lifecycle compatibility gap. + +## Action Plan + +1. Keep the fix in the imported N-API bridge, not in `SignalWrap`. +2. Preserve the existing guest ABI shape for ordinary wraps. +3. Teach the bridge whether the guest supplied a finalizer. +4. Use a host-side no-op finalizer shim when the guest supplied one, so V8's + `napi_wrap(..., result)` precondition is satisfied. +5. Rebuild the source Wasmer CLI with the `napi-v8` feature. +6. Verify: + +```sh +wasix/build-wasix.sh +wasmer run --experimental-napi -l . +wasmer run --experimental-napi . -- -e "console.log('hello from imports')" +``` + +For parity with the source Wasmer checkout, rebuild Wasmer with the `napi-v8` +feature before comparing runtime behavior. + +## Fix + +Implemented in the imported N-API bridge: + +- `guest_napi_wrap(...)` and `guest_napi_add_finalizer(...)` now pass a + `has_finalize_cb` flag to the host bridge. +- `snapi_bridge_wrap(...)` supplies `NoopGuestFinalizer` when the guest provided + a finalizer pointer, which satisfies the V8 backend rule that + `napi_wrap(..., result)` requires a non-null finalizer. +- `snapi_bridge_add_finalizer(...)` uses the same no-op host finalizer when the + guest supplied a finalizer pointer. + +This preserves imported object wrapping and unwrapping for native Edge wrapper +classes such as `SignalWrap`. It does not yet invoke the guest's actual +finalizer callback. + +## Verification + +Direct C++ syntax verification passed against the Wasmer checkout's generated +V8 include directory: + +```sh +c++ -std=c++20 -fsyntax-only -DNAPI_EXTERN= -DV8_COMPRESS_POINTERS=1 \ + -I ~/src/dev/wasmer/target/debug/build/wasmer-napi-4a90baddef866940/out/v8-prebuilt/11.9.2/darwin-arm64/include \ + -I ~/src/dev/wasmer/lib/napi/include \ + -I ~/src/dev/wasmer/lib/napi/lib/src \ + -I ~/src/dev/wasmer/lib/napi/v8/src \ + ~/src/dev/wasmer/lib/napi/src/napi_bridge_init.cc +``` + +The check emitted only the existing `napi_float16_array` switch warning. + +The source Wasmer CLI was rebuilt with N-API enabled: + +```sh +cargo build --release -p wasmer-cli \ + --features llvm,napi-v8,wasmer-artifact-create,static-artifact-create,wasmer-artifact-load,static-artifact-load \ + --bin wasmer +``` + +The rebuilt binary passed the imported N-API smoke command: + +```sh +~/src/dev/wasmer/target/release/wasmer run --experimental-napi . -- -e "console.log('hello from imports')" +``` + +Output: + +```text +hello from imports +``` + +The original REPL startup command now reaches the prompt: + +```sh +~/src/dev/wasmer/target/release/wasmer run --experimental-napi -l . +``` + +Output reached: + +```text +Welcome to Edge.js 0.0.0-faf7e02 (Node.js v24.13.2). +Type ".help" for more information. +> +``` + +## Remaining Caveats + +Sending Ctrl-C to the REPL after startup produced a separate signal callback +runtime error: + +```text +signal handler runtime error: RuntimeError: indirect call type mismatch +``` + +Treat that as a follow-up signal callback/trampoline issue. It is distinct from +the startup `Illegal invocation`, which was caused by failed `napi_wrap(...)`. diff --git a/plans/quickjs-wasm/development/troubleshooting/node-compat/napi/019_imported_napi_repl_input_stall.md b/plans/quickjs-wasm/development/troubleshooting/node-compat/napi/019_imported_napi_repl_input_stall.md new file mode 100644 index 000000000..c1ca1e40a --- /dev/null +++ b/plans/quickjs-wasm/development/troubleshooting/node-compat/napi/019_imported_napi_repl_input_stall.md @@ -0,0 +1,228 @@ +# Imported N-API REPL input stall + +| | | Remarks | +| --- | --- | --- | +| **Status** | ▶️ | Active investigation after imported N-API startup reaches the REPL prompt but does not evaluate input. | +| **Severity** | High | Blocks interactive REPL use for the root V8/imports WASIX package under Wasmer. | + +## Failure + +After the imported N-API wrap/finalizer fix, the root package starts: + +```sh +~/src/dev/wasmer/target/release/wasmer run --experimental-napi -l . +``` + +It prints: + +```text +Welcome to Edge.js 0.0.0-faf7e02 (Node.js v24.13.2). +Type ".help" for more information. +> +``` + +Typing: + +```js +console.log("hello") +``` + +echoes the line but does not evaluate it or print a new prompt. `Ctrl-D` also +does not exit cleanly. `Ctrl-C` terminates the host run and logs: + +```text +signal handler runtime error: RuntimeError: indirect call type mismatch +``` + +Disabling persistent history does not make input evaluate: + +```sh +NODE_REPL_HISTORY="" ~/src/dev/wasmer/target/release/wasmer run --experimental-napi -l . +``` + +This distinguishes the failure from the old embedded QuickJS REPL history stall, +where disabling history confirmed the promise-continuation diagnosis. + +## Action Plan + +1. Keep this separate from the prior `napi_wrap(...)` startup fix. +2. Trace the imported N-API callback path used when host V8 invokes a JS + function backed by a guest WASM callback. +3. Verify whether `generic_wasm_callback(...)` has a valid callback context when + callbacks fire from WASIX/libuv events such as TTY input and signal delivery. +4. Inspect the function table typing for `snapi_host_invoke_wasm_callback(...)` + and the guest callback function pointers stored by `snapi_bridge_register_callback(...)`. +5. Add a narrow imported-NAPI smoke test that exercises an event-driven callback + after startup, not just synchronous `console.log(...)`. +6. Rebuild source Wasmer with `napi-v8`, then verify: + +```sh +~/src/dev/wasmer/target/release/wasmer run --experimental-napi . -- -e "setTimeout(() => console.log('timer'), 0)" +NODE_REPL_HISTORY="" ~/src/dev/wasmer/target/release/wasmer run --experimental-napi -l . +~/src/dev/wasmer/target/release/wasmer run --experimental-napi -l . +``` + +Expected REPL behavior: entered expressions evaluate, a new prompt appears, and +EOF exits. + +## Findings + +This is not the old QuickJS microtask/history issue: + +- Embedded QuickJS WASIX still runs timers and immediates under Wasmer: + +```sh +cd ~/src/dev/edgejs/quickjs-wasm +wasmer run . -- -e "setTimeout(() => console.log('timer'), 10); console.log('scheduled')" +wasmer run . -- -e "setImmediate(()=>console.log('immediate')); Promise.resolve().then(()=>console.log('promise'));" +``` + +Both commands print the expected asynchronous output. + +- Imported N-API with source Wasmer drains V8 promise microtasks: + +```sh +~/src/dev/wasmer/target/release/wasmer run --experimental-napi . -- -e "Promise.resolve().then(() => console.log('promise'))" +``` + +This prints `promise`. + +- Imported N-API does not run libuv-backed work: + +```sh +~/src/dev/wasmer/target/release/wasmer run --experimental-napi . -- -e "setTimeout(() => console.log('timer'), 10); console.log('scheduled')" +~/src/dev/wasmer/target/release/wasmer run --experimental-napi . -- -e "require('fs').promises.open('/tmp/edge-imports-repl-probe','a+').then(h=>h.close()).then(()=>console.log('closed'))" +``` + +The timer command prints only `scheduled`; the fs promise never reaches +`closed`. + +- `setImmediate` is also broken in imported mode: + +```sh +~/src/dev/wasmer/target/release/wasmer run --experimental-napi . -- -e "setImmediate(()=>console.log('immediate')); Promise.resolve().then(()=>console.log('promise'));" +``` + +It prints `promise` and then stalls. + +- Direct timer binding calls are present and callable: + +```sh +~/src/dev/wasmer/target/release/wasmer run --experimental-napi . -- -e "const b=internalBinding('timers'); console.log('types', typeof b.setupTimers, typeof b.scheduleTimer, typeof b.toggleTimerRef); b.scheduleTimer(10); b.toggleTimerRef(true); console.log('done')" +``` + +This prints `types function function function` and `done`, but no timer callback. + +- A temporary Wasmer-side callback trace showed `setupTimers`, `scheduleTimer`, + and `toggleTimerRef` are invoked with an active callback context during + bootstrap and top-level eval. The problem is therefore below simple + `generic_wasm_callback(...)` dispatch for synchronous native binding calls. + +- Temporarily enabling `enable_blocking_sleep` while keeping N-API async + threading disabled did not fix timers and caused broader hangs. Removing the + N-API async-threading override entirely also caused early abnormal exit. These + were reverted. + +Current working hypothesis: imported N-API synchronous callbacks can enter the +guest, and V8 promise microtasks drain, but guest libuv/event-loop liveness or +event callback delivery is not preserved in the imported-NAPI WASIX run. The +REPL symptom is likely the same class as timers/fs/immediate: stdin/readline is +waiting on libuv work that never reaches the JS callback. + +### LLDB / debug Wasmer findings + +Built a debug Wasmer CLI with the same imported N-API and LLVM feature set: + +```sh +cd ~/src/dev/wasmer +cargo build -p wasmer-cli --features llvm,napi-v8,wasmer-artifact-create,static-artifact-create,wasmer-artifact-load,static-artifact-load --bin wasmer +``` + +The debug binary reproduces the failure on the LLVM path: + +```sh +~/src/dev/wasmer/target/debug/wasmer run --wasmer-dir /private/tmp/wasmer-debug-cache --disable-cache --experimental-napi -l . -- -e "console.log('sync'); Promise.resolve().then(()=>console.log('promise')); setTimeout(()=>console.log('timer'),10); console.log('scheduled')" +``` + +It prints `sync`, `scheduled`, and `promise`, but not `timer`. + +LLDB confirms the imported N-API runner disables WASIX asynchronous threading +and enters the guest synchronously: + +```text +ContextSwitchingEnvironment::run_main_context + context_switching.rs:121 let result = entrypoint.call(&mut store, ¶ms); +``` + +So Wasmer is not directly running Edge's JavaScript loop. The guest EdgeJS wasm +calls its own compiled-in libuv `uv_run(...)`; Wasmer executes that guest code, +and guest libuv crosses into native Wasmer only for WASIX syscalls such as +`poll_oneoff`. With `-l/--llvm`, the guest wasm frames are LLVM-generated native +code, so source-level breakpoints are reliable at the host WASIX/N-API import +boundary rather than inside guest EdgeJS C++. + +Wasmer-side tracing and LLDB both show that the guest loop does reach WASIX +polling after `setTimeout(...)` is scheduled: + +```text +poll_oneoff(..., nsubscriptions=3) +``` + +The `RUST_LOG=wasmer_wasix::syscalls::wasi::poll_oneoff=trace` run reports +successful clock events, but `NODE_DEBUG=timer` only logs insertion: + +```text +TIMER 1: no 10 list was found in insert, creating a new one +``` + +It never logs `process timer lists` or `timeout callback`. + +Breakpoint-hit comparison under LLDB: + +```text +no timer: + snapi_bridge_call_function: 164 + generic_wasm_callback: 218 + poll_oneoff: 9 + +setTimeout(..., 10): + snapi_bridge_call_function: 171 + generic_wasm_callback: 260 + poll_oneoff: 10 +``` + +Skipping the no-timer baseline and printing names for the seven extra +`snapi_bridge_call_function(...)` hits produced: + +```text +processTicksAndRejections +emit +internalBinding +emit +emitDestroyNative +emitDestroyNative +internalBinding +``` + +`processTimers` does not appear. This means the timer-specific extra JS calls +are nextTick/lifecycle/cleanup work, not the JS timer dispatcher. + +Current narrowed diagnosis: the guest EdgeJS event loop reaches native Wasmer +WASIX polling and receives clock readiness, but Edge's native timer callback +does not get as far as calling the JS `processTimers(now)` dispatcher. The next +breakpoint should be inside the guest Edge timer path if debug info permits, or +a temporary trace in `src/edge_environment.cc::Environment::OnTimer` / +`src/edge_timers_host.cc::CallTimersCallback` should verify whether `OnTimer` +fires, whether `can_call_into_js()` rejects the callback, whether the timers +callback reference is missing, or whether `EdgeMakeCallbackWithFlags(...)` +returns without invoking `processTimers`. + +## Verification State + +- Source Wasmer was rebuilt with the imported N-API finalizer ABI fix restored. +- `console.log('plain')` works under imported N-API after the first-run Wasmer + compile/cache delay. +- `setTimeout` still prints only the synchronous line. +- The local `~/.wasmer` compiled-module cache write is blocked in the sandbox, + so first runs after rebuilding `build-wasix/edgejs.wasm` can pause before any + Edge bootstrap trace appears. From 80ecab090721cf6218918c689241478134be9c61 Mon Sep 17 00:00:00 2001 From: Sadhbh Code Date: Mon, 15 Jun 2026 12:36:46 +0100 Subject: [PATCH 28/65] Updated wasix test plans --- .../wasix-plan/libuv-wasix/01_use_posix_spawn_on_wasix.md | 2 +- ...02_udp_disconnect_and_multicast_option_compatibility.md | 2 +- .../libuv-wasix/03_tcp_keepalive_timing_options.md | 4 ++++ ...uickjs_structured_clone_serdes_for_sharedarraybuffer.md | 2 +- plans/wasix-plan/napi/02_quickjs_napi_callsite_frames.md | 7 ------- .../01_wasi_stack_limit_should_match_non_wasi_quickjs.md | 3 +-- .../04_posix_st_mode_for_files_and_directories.md | 4 ++-- plans/wasix-plan/wasix-libc/05_loopback_reverse_lookup.md | 4 ++-- .../01_udp_datagram_receive_readiness_and_backlog.md | 3 --- .../wasmer/04_proc_spawn3_proc_exec4_for_real_argv_envp.md | 3 +-- ...6_future_sendmsg_recvmsg_control_data_and_fd_passing.md | 2 +- 11 files changed, 14 insertions(+), 22 deletions(-) diff --git a/plans/wasix-plan/libuv-wasix/01_use_posix_spawn_on_wasix.md b/plans/wasix-plan/libuv-wasix/01_use_posix_spawn_on_wasix.md index 43133b943..4321c9bed 100644 --- a/plans/wasix-plan/libuv-wasix/01_use_posix_spawn_on_wasix.md +++ b/plans/wasix-plan/libuv-wasix/01_use_posix_spawn_on_wasix.md @@ -82,6 +82,6 @@ stdio container into `posix_spawn_file_actions_*` calls plus `posix_spawnp()`. ## Proposed Solution References -### No PR group +### Commits without PR - libuv-wasix [ba06698b](https://github.com/Anodized-Titanium/libuv-wasix/commit/ba06698ba3f3b508a7a4eeacb23e0c911ebf4576) libuv-wasix: use WASIX posix_spawn/proc_join for child processes diff --git a/plans/wasix-plan/libuv-wasix/02_udp_disconnect_and_multicast_option_compatibility.md b/plans/wasix-plan/libuv-wasix/02_udp_disconnect_and_multicast_option_compatibility.md index de1099c5c..e9de2244b 100644 --- a/plans/wasix-plan/libuv-wasix/02_udp_disconnect_and_multicast_option_compatibility.md +++ b/plans/wasix-plan/libuv-wasix/02_udp_disconnect_and_multicast_option_compatibility.md @@ -92,6 +92,6 @@ multicast implementation remains a Wasmer task. ## Proposed Solution References -### No PR group +### Commits without PR - libuv-wasix [0ae770b9](https://github.com/Anodized-Titanium/libuv-wasix/commit/0ae770b9daae35d97d3c9a2693a5b22362124f12) multicast TTL/loop/interface shims and UDP disconnect handling diff --git a/plans/wasix-plan/libuv-wasix/03_tcp_keepalive_timing_options.md b/plans/wasix-plan/libuv-wasix/03_tcp_keepalive_timing_options.md index 1db0eed48..e7d875d6d 100644 --- a/plans/wasix-plan/libuv-wasix/03_tcp_keepalive_timing_options.md +++ b/plans/wasix-plan/libuv-wasix/03_tcp_keepalive_timing_options.md @@ -78,3 +78,7 @@ claiming that WASIX already implements every TCP timing option. ### [wasmerio/edgejs#91: [WIP] Node tests using Edgejs WASIX QuickJS](https://github.com/wasmerio/edgejs/pull/91) - edgejs [9fa61f18](https://github.com/wasmerio/edgejs/commit/9fa61f1888f34c0785f54da8dd99ae193e536439) UV keep alive fix (disable unsupported options) + +### Commits without PR + +- libuv-wasix [8d537440](https://github.com/Anodized-Titanium/libuv-wasix/commit/8d537440533cfc290e33c7bcbf181ab414dd1850) Wasix-LibC supports SOL_SOCKET + SO_KEEPALIVE, however does not support additional options such as TCP_KEEPIDLE, TCP_KEEPINTVL, or TCP_KEEPCNT - so we disable them diff --git a/plans/wasix-plan/napi/01_quickjs_structured_clone_serdes_for_sharedarraybuffer.md b/plans/wasix-plan/napi/01_quickjs_structured_clone_serdes_for_sharedarraybuffer.md index d1629578a..8d9163a70 100644 --- a/plans/wasix-plan/napi/01_quickjs_structured_clone_serdes_for_sharedarraybuffer.md +++ b/plans/wasix-plan/napi/01_quickjs_structured_clone_serdes_for_sharedarraybuffer.md @@ -92,6 +92,6 @@ bridges QuickJS SAB lifetime with Node/N-API ownership. - edgejs [38e01f79](https://github.com/wasmerio/edgejs/commit/38e01f79c15636badbab17faf6c31eef7d16abc6) Napi/Qjs: SharedArrayBuffer. Ssl certs in wasix tests. -### No PR group +### Commits without PR - napi [21f22e3](https://github.com/wasmerio/napi/commit/21f22e3cc3b90a2c453d2e4115e0db42c5d8f68a) QJS: Serdes for SharedArrayBuffer diff --git a/plans/wasix-plan/napi/02_quickjs_napi_callsite_frames.md b/plans/wasix-plan/napi/02_quickjs_napi_callsite_frames.md index f6ac581bc..c23fc0274 100644 --- a/plans/wasix-plan/napi/02_quickjs_napi_callsite_frames.md +++ b/plans/wasix-plan/napi/02_quickjs_napi_callsite_frames.md @@ -84,10 +84,3 @@ JavaScript Error.captureStackTrace()/diagnostics path This keeps stack-frame semantics in the JS engine integration, not in each EdgeJS feature that happens to need caller metadata. - -## Proposed Solution References - -### No PR group - -- quickjs [41b00d4](https://github.com/wasmerio/quickjs/commit/41b00d4cc34cf79188cd9255f050e95ea1a2e9d6) Added callsites into QuickJS -- napi [5a5c3b2](https://github.com/wasmerio/napi/commit/5a5c3b283aa9b37e53898543fb33ee0255a43e1e) Improved quickjs callsites similar to v8 diff --git a/plans/wasix-plan/quickjs/01_wasi_stack_limit_should_match_non_wasi_quickjs.md b/plans/wasix-plan/quickjs/01_wasi_stack_limit_should_match_non_wasi_quickjs.md index 814201fa5..699391a16 100644 --- a/plans/wasix-plan/quickjs/01_wasi_stack_limit_should_match_non_wasi_quickjs.md +++ b/plans/wasix-plan/quickjs/01_wasi_stack_limit_should_match_non_wasi_quickjs.md @@ -83,7 +83,6 @@ shape of JavaScript stack overflow errors. ## Proposed Solution References -### No PR group +### Commits without PR - quickjs [9a59f17](https://github.com/wasmerio/quickjs/commit/9a59f17a026ebc3b6932afa886c21ebf02689a2e) Set WASI stack limit same as non-WASI -- quickjs [cec4427](https://github.com/wasmerio/quickjs/commit/cec44274d0b0abb9670e978a32ed4b4c2dd3b0bd) Improved stack setter diff --git a/plans/wasix-plan/wasix-libc/04_posix_st_mode_for_files_and_directories.md b/plans/wasix-plan/wasix-libc/04_posix_st_mode_for_files_and_directories.md index 730dd448b..4d7a94cb4 100644 --- a/plans/wasix-plan/wasix-libc/04_posix_st_mode_for_files_and_directories.md +++ b/plans/wasix-plan/wasix-libc/04_posix_st_mode_for_files_and_directories.md @@ -107,6 +107,6 @@ st->st_mode = mode; ## Proposed Solution References -### No PR group +### Commits without PR -- wasix-libc [57b1083](https://github.com/Anodized-Titanium/wasix-libc/commit/57b108379c93b2847d5d8b373067a54f0f083293) Fix dir and file flags +- wasix-libc [631ef5d5](https://github.com/Anodized-Titanium/wasix-libc/commit/631ef5d5289a56b584fd84e4296627879d8d5e5c) Fix dir and file flags diff --git a/plans/wasix-plan/wasix-libc/05_loopback_reverse_lookup.md b/plans/wasix-plan/wasix-libc/05_loopback_reverse_lookup.md index 79a6a26ed..d531dc39a 100644 --- a/plans/wasix-plan/wasix-libc/05_loopback_reverse_lookup.md +++ b/plans/wasix-plan/wasix-libc/05_loopback_reverse_lookup.md @@ -100,6 +100,6 @@ case AF_INET6: ## Proposed Solution References -### No PR group +### Commits without PR -- wasix-libc [f157cd9](https://github.com/Anodized-Titanium/wasix-libc/commit/f157cd9aaa3ee6c3e6aafa4b2afb6192b543783b) Reverse loopback +- wasix-libc [1a028f0e9](https://github.com/Anodized-Titanium/wasix-libc/commit/1a028f0e9aeaffa580fc6bc5962069531fda322d) Reverse loopback diff --git a/plans/wasix-plan/wasmer/01_udp_datagram_receive_readiness_and_backlog.md b/plans/wasix-plan/wasmer/01_udp_datagram_receive_readiness_and_backlog.md index c1bc709c4..a7043b152 100644 --- a/plans/wasix-plan/wasmer/01_udp_datagram_receive_readiness_and_backlog.md +++ b/plans/wasix-plan/wasmer/01_udp_datagram_receive_readiness_and_backlog.md @@ -196,6 +196,3 @@ guest buffer. - wasmer [66552d27be9](https://github.com/Anodized-Titanium/wasmer/commit/66552d27be9a7efc7962aab22bd60375476c07bb) Fix merge conflict mistake - wasmer [8e92612e917](https://github.com/Anodized-Titanium/wasmer/commit/8e92612e917cb9745f21aab2634dd7687c229d33) Merge branch 'main' into udp-datagram-receive-and-last-err -### No PR group - -- wasmer [bee1e0ab8ae](https://github.com/Anodized-Titanium/wasmer/commit/bee1e0ab8aef47c6794d89fd5263f9301cc5837b) Merge branch 'udp-datagram-receive-and-last-err' into tmp-work-4 diff --git a/plans/wasix-plan/wasmer/04_proc_spawn3_proc_exec4_for_real_argv_envp.md b/plans/wasix-plan/wasmer/04_proc_spawn3_proc_exec4_for_real_argv_envp.md index d8a982803..b77bf2c86 100644 --- a/plans/wasix-plan/wasmer/04_proc_spawn3_proc_exec4_for_real_argv_envp.md +++ b/plans/wasix-plan/wasmer/04_proc_spawn3_proc_exec4_for_real_argv_envp.md @@ -132,11 +132,10 @@ spawn_process(path, argv, envp, file_actions) ## Proposed Solution References -### No PR group +### Commits without PR - wasmer [d80c3d8c980](https://github.com/Anodized-Titanium/wasmer/commit/d80c3d8c98065562c845c2ec8acfe0668798a166) Add proc_spawn3 and proc_exec4... - wasmer [b5880b5268b](https://github.com/Anodized-Titanium/wasmer/commit/b5880b5268b74aefc5b4295767286976d83b0deb) apply review comments - wasmer [e9d1478b914](https://github.com/Anodized-Titanium/wasmer/commit/e9d1478b914c224051541d35281ff0a0638fab21) fix tests -- wasmer [bda2ead2504](https://github.com/Anodized-Titanium/wasmer/commit/bda2ead2504246057a4d91d3dc5b7b071e7ed9bd) Merge branch 'feat/wasix-proc-spawn3' into tmp-work-4 - wasix-libc proc_spawn3/proc_exec4 lowering - libuv-wasix [ba06698b](https://github.com/Anodized-Titanium/libuv-wasix/commit/ba06698ba3f3b508a7a4eeacb23e0c911ebf4576) libuv-wasix: use WASIX posix_spawn/proc_join... diff --git a/plans/wasix-plan/wasmer/06_future_sendmsg_recvmsg_control_data_and_fd_passing.md b/plans/wasix-plan/wasmer/06_future_sendmsg_recvmsg_control_data_and_fd_passing.md index 54000e411..15dc84086 100644 --- a/plans/wasix-plan/wasmer/06_future_sendmsg_recvmsg_control_data_and_fd_passing.md +++ b/plans/wasix-plan/wasmer/06_future_sendmsg_recvmsg_control_data_and_fd_passing.md @@ -146,7 +146,7 @@ more honest than pretending descriptor passing worked. ## Proposed Solution References -### No PR group +### Commits without PR - WITX/libc payload ABI exists as plan/input. - Phase 2 SCM_RIGHTS/handle passing remains a Wasmer runtime task. From 499c4fcc9743b2d56c130ed623be816b70bf0537 Mon Sep 17 00:00:00 2001 From: Sadhbh Code Date: Mon, 15 Jun 2026 12:44:07 +0100 Subject: [PATCH 29/65] Updated wasix test plans --- .../wasmer/01_udp_datagram_receive_readiness_and_backlog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plans/wasix-plan/wasmer/01_udp_datagram_receive_readiness_and_backlog.md b/plans/wasix-plan/wasmer/01_udp_datagram_receive_readiness_and_backlog.md index a7043b152..131d61b32 100644 --- a/plans/wasix-plan/wasmer/01_udp_datagram_receive_readiness_and_backlog.md +++ b/plans/wasix-plan/wasmer/01_udp_datagram_receive_readiness_and_backlog.md @@ -194,5 +194,5 @@ guest buffer. - wasmer [c56897f3bec](https://github.com/Anodized-Titanium/wasmer/commit/c56897f3beced2c9c4861137bf81f32320a14b7c) Fixed UDP socket dgram receive - wasmer [65e0eb571ef](https://github.com/Anodized-Titanium/wasmer/commit/65e0eb571ef4fde1aee765fae2955956dcbd1d8f) Peek UDP packets correctly - wasmer [66552d27be9](https://github.com/Anodized-Titanium/wasmer/commit/66552d27be9a7efc7962aab22bd60375476c07bb) Fix merge conflict mistake -- wasmer [8e92612e917](https://github.com/Anodized-Titanium/wasmer/commit/8e92612e917cb9745f21aab2634dd7687c229d33) Merge branch 'main' into udp-datagram-receive-and-last-err +- wasmer [8e92612e917](https://github.com/Anodized-Titanium/wasmer/commit/8e92612e917cb9745f21aab2634dd7687c229d33) Conflict resolution for UDP receive/readiness changes in `lib/virtual-net/src/{client,host,lib}.rs` and `lib/wasix/src/net/socket.rs` From 0b03d80fec9ed38bc3e3df4da844f2d85aedfa8d Mon Sep 17 00:00:00 2001 From: Sadhbh Code Date: Mon, 15 Jun 2026 12:55:41 +0100 Subject: [PATCH 30/65] More detailed examples --- ...pe_flags_sock_nonblock_and_sock_cloexec.md | 52 +++++++++++++++++- ..._boolean_socket_option_pointer_handling.md | 42 ++++++++++++++- .../wasix-libc/03_getsockopt_so_error.md | 54 +++++++++++++++++-- 3 files changed, 141 insertions(+), 7 deletions(-) diff --git a/plans/wasix-plan/wasix-libc/01_socket_type_flags_sock_nonblock_and_sock_cloexec.md b/plans/wasix-plan/wasix-libc/01_socket_type_flags_sock_nonblock_and_sock_cloexec.md index bb187286d..061b89d82 100644 --- a/plans/wasix-plan/wasix-libc/01_socket_type_flags_sock_nonblock_and_sock_cloexec.md +++ b/plans/wasix-plan/wasix-libc/01_socket_type_flags_sock_nonblock_and_sock_cloexec.md @@ -14,10 +14,58 @@ flags, the fd remains blocking and code that expects `EAGAIN` can hang. Minimal Example: +This is a complete C program that asks libc for a UDP socket that is both +nonblocking and close-on-exec at creation time. Without the fix, libc can pass +the combined type bits to WASIX as if they were the base socket type, or it can +create the socket but lose one of the requested flags. + ```c +#include +#include +#include +#include #include - -int fd = socket(AF_INET, SOCK_DGRAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); +#include + +int main(void) { + int fd = socket(AF_INET, SOCK_DGRAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); + if (fd < 0) { + perror("socket"); + return 1; + } + + int status_flags = fcntl(fd, F_GETFL, 0); + if (status_flags < 0) { + perror("fcntl(F_GETFL)"); + return 1; + } + if ((status_flags & O_NONBLOCK) == 0) { + fprintf(stderr, "socket is not nonblocking\n"); + return 1; + } + + int fd_flags = fcntl(fd, F_GETFD, 0); + if (fd_flags < 0) { + perror("fcntl(F_GETFD)"); + return 1; + } + if ((fd_flags & FD_CLOEXEC) == 0) { + fprintf(stderr, "socket is not close-on-exec\n"); + return 1; + } + + char byte; + ssize_t nread = recv(fd, &byte, sizeof(byte), 0); + if (nread < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) { + puts("socket flags are visible through POSIX APIs"); + close(fd); + return 0; + } + + fprintf(stderr, "expected nonblocking recv, got nread=%zd errno=%d (%s)\n", + nread, errno, strerror(errno)); + return 1; +} ``` When it occurs: diff --git a/plans/wasix-plan/wasix-libc/02_boolean_socket_option_pointer_handling.md b/plans/wasix-plan/wasix-libc/02_boolean_socket_option_pointer_handling.md index d80d54e9d..90c72032f 100644 --- a/plans/wasix-plan/wasix-libc/02_boolean_socket_option_pointer_handling.md +++ b/plans/wasix-plan/wasix-libc/02_boolean_socket_option_pointer_handling.md @@ -19,9 +19,47 @@ When it occurs: Minimal Example: +This is a complete C program for the specific bug shape: the caller passes a +pointer to an integer value of `0`. Libc must read `*optval`; it must not treat +the pointer address itself as the boolean value. + ```c -int off = 0; -setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &off, sizeof(off)); +#include +#include +#include +#include +#include +#include + +int main(void) { + int fd = socket(AF_INET6, SOCK_STREAM, 0); + if (fd < 0) { + perror("socket"); + return 1; + } + + int off = 0; + if (setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &off, sizeof(off)) < 0) { + perror("setsockopt(IPV6_V6ONLY=0)"); + return 1; + } + + int value = -1; + socklen_t value_len = sizeof(value); + if (getsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &value, &value_len) < 0) { + perror("getsockopt(IPV6_V6ONLY)"); + return 1; + } + + if (value != 0) { + fprintf(stderr, "expected IPV6_V6ONLY=0, got %d\n", value); + return 1; + } + + puts("boolean socket option used pointed-to value"); + close(fd); + return 0; +} ``` Callgraph and boundary: diff --git a/plans/wasix-plan/wasix-libc/03_getsockopt_so_error.md b/plans/wasix-plan/wasix-libc/03_getsockopt_so_error.md index 1116300cd..ec6b00752 100644 --- a/plans/wasix-plan/wasix-libc/03_getsockopt_so_error.md +++ b/plans/wasix-plan/wasix-libc/03_getsockopt_so_error.md @@ -13,10 +13,58 @@ expect. Minimal Example: +This is a complete C program that uses a nonblocking connect and then asks libc +for the pending socket error through the normal POSIX `SO_ERROR` path. Without +the fix, libc can report success or a generic value instead of the runtime's +last socket error. + ```c -int err = 0; -socklen_t len = sizeof(err); -getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &len); +#include +#include +#include +#include +#include +#include +#include +#include +#include + +int main(void) { + int fd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); + if (fd < 0) { + perror("socket"); + return 1; + } + + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons(1); + if (inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr) != 1) { + perror("inet_pton"); + return 1; + } + + int rc = connect(fd, (struct sockaddr *)&addr, sizeof(addr)); + if (rc < 0 && errno != EINPROGRESS) { + perror("connect"); + return 1; + } + + struct pollfd pfd = { .fd = fd, .events = POLLOUT }; + poll(&pfd, 1, 1000); + + int err = 0; + socklen_t len = sizeof(err); + if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &len) < 0) { + perror("getsockopt(SO_ERROR)"); + return 1; + } + + printf("SO_ERROR=%d (%s), len=%u\n", err, strerror(err), (unsigned)len); + close(fd); + return err == 0 ? 1 : 0; +} ``` Callgraph and boundary: From cea16e54b8747b06689beeffc105f155cb7bb034 Mon Sep 17 00:00:00 2001 From: Sadhbh Code Date: Mon, 15 Jun 2026 15:16:10 +0100 Subject: [PATCH 31/65] Updated wasix test plans --- ...quest_lifecycle_and_address_family_selection.md | 4 ++-- ...ificate_files_instead_of_hard_coding_answers.md | 4 ++-- ...est_workarounds_after_lower_layer_plans_land.md | 6 +++--- ...openssl_error_mapping_and_version_capability.md | 4 ++-- .../05_runner_determinism_and_workflow_setup.md | 14 +++++++------- .../libuv-wasix/01_use_posix_spawn_on_wasix.md | 14 ++++++++++---- ...isconnect_and_multicast_option_compatibility.md | 14 +++++++++++++- .../libuv-wasix/03_tcp_keepalive_timing_options.md | 4 ++-- plans/wasix-plan/libuv-wasix/README.md | 11 +++++++++++ ...tructured_clone_serdes_for_sharedarraybuffer.md | 4 ++-- ...si_stack_limit_should_match_non_wasi_quickjs.md | 2 +- ...et_type_flags_sock_nonblock_and_sock_cloexec.md | 2 +- .../02_boolean_socket_option_pointer_handling.md | 2 +- .../wasix-libc/03_getsockopt_so_error.md | 4 ++-- .../04_posix_st_mode_for_files_and_directories.md | 2 +- .../wasix-libc/05_loopback_reverse_lookup.md | 2 +- ...1_udp_datagram_receive_readiness_and_backlog.md | 8 ++++---- ...2_connected_udp_send_peer_state_and_emsgsize.md | 4 ++-- .../wasmer/03_last_socket_error_so_error.md | 4 ++-- ...04_proc_spawn3_proc_exec4_for_real_argv_envp.md | 11 +++++++---- .../wasmer/05_partial_success_for_fd_write.md | 2 +- 21 files changed, 77 insertions(+), 45 deletions(-) diff --git a/plans/wasix-plan/edgejs/01_c_ares_request_lifecycle_and_address_family_selection.md b/plans/wasix-plan/edgejs/01_c_ares_request_lifecycle_and_address_family_selection.md index a9925cb49..3e4ecf4be 100644 --- a/plans/wasix-plan/edgejs/01_c_ares_request_lifecycle_and_address_family_selection.md +++ b/plans/wasix-plan/edgejs/01_c_ares_request_lifecycle_and_address_family_selection.md @@ -129,5 +129,5 @@ and preserve the requested address family when constructing c-ares work. ### [wasmerio/edgejs#91: [WIP] Node tests using Edgejs WASIX QuickJS](https://github.com/wasmerio/edgejs/pull/91) -- edgejs [929a9151](https://github.com/wasmerio/edgejs/commit/929a9151c336b5862839d7b3d3e2bec219f852ca) c-ares requests are now tracked per channel, cancellation completes still-active requests with ECANCELLED, reverse lookup includes h_name, and loopback reverse/nameinfo returns localhost -- edgejs [64faa6ca](https://github.com/wasmerio/edgejs/commit/64faa6caba3648b7393c69c9a339a4b90f2a697e) Respect address family (IPv4 or IPv6) when asked GetAddrInfo +- Sadhbh: edgejs [929a9151](https://github.com/wasmerio/edgejs/commit/929a9151c336b5862839d7b3d3e2bec219f852ca) c-ares requests are now tracked per channel, cancellation completes still-active requests with ECANCELLED, reverse lookup includes h_name, and loopback reverse/nameinfo returns localhost +- Sadhbh: edgejs [64faa6ca](https://github.com/wasmerio/edgejs/commit/64faa6caba3648b7393c69c9a339a4b90f2a697e) Respect address family (IPv4 or IPv6) when asked GetAddrInfo diff --git a/plans/wasix-plan/edgejs/02_mount_resolver_and_certificate_files_instead_of_hard_coding_answers.md b/plans/wasix-plan/edgejs/02_mount_resolver_and_certificate_files_instead_of_hard_coding_answers.md index 75398cef5..d4d0d3b19 100644 --- a/plans/wasix-plan/edgejs/02_mount_resolver_and_certificate_files_instead_of_hard_coding_answers.md +++ b/plans/wasix-plan/edgejs/02_mount_resolver_and_certificate_files_instead_of_hard_coding_answers.md @@ -117,5 +117,5 @@ logic. ### [wasmerio/edgejs#91: [WIP] Node tests using Edgejs WASIX QuickJS](https://github.com/wasmerio/edgejs/pull/91) -- edgejs [dc9a0465](https://github.com/wasmerio/edgejs/commit/dc9a046509ff36b4f1e952967528480a5873e4b2) Added /etc/hosts, removed stream type normalization -- edgejs [38e01f79](https://github.com/wasmerio/edgejs/commit/38e01f79c15636badbab17faf6c31eef7d16abc6) Napi/Qjs: SharedArrayBuffer. Ssl certs in wasix tests. +- Sadhbh: edgejs [dc9a0465](https://github.com/wasmerio/edgejs/commit/dc9a046509ff36b4f1e952967528480a5873e4b2) Added /etc/hosts, removed stream type normalization +- Sadhbh: edgejs [38e01f79](https://github.com/wasmerio/edgejs/commit/38e01f79c15636badbab17faf6c31eef7d16abc6) Napi/Qjs: SharedArrayBuffer. Ssl certs in wasix tests. diff --git a/plans/wasix-plan/edgejs/03_remove_wasix_only_guest_workarounds_after_lower_layer_plans_land.md b/plans/wasix-plan/edgejs/03_remove_wasix_only_guest_workarounds_after_lower_layer_plans_land.md index 169728826..eb50c973c 100644 --- a/plans/wasix-plan/edgejs/03_remove_wasix_only_guest_workarounds_after_lower_layer_plans_land.md +++ b/plans/wasix-plan/edgejs/03_remove_wasix_only_guest_workarounds_after_lower_layer_plans_land.md @@ -124,6 +124,6 @@ branch. ### [wasmerio/edgejs#91: [WIP] Node tests using Edgejs WASIX QuickJS](https://github.com/wasmerio/edgejs/pull/91) -- edgejs [f1999f45](https://github.com/wasmerio/edgejs/commit/f1999f45d43453d508db45b3b52e4115983e26a8) Removed hacks: loopback, and spawn -- edgejs [27118329](https://github.com/wasmerio/edgejs/commit/27118329f47b9876c3f415bf42669f9cc4a6568b) Fixes around edge process wrap, plus w/a for edge -e eval -- edgejs [61ba9604](https://github.com/wasmerio/edgejs/commit/61ba9604327a7326a555c2d537d7214421c60a9c) various fixes +- Sadhbh: edgejs [f1999f45](https://github.com/wasmerio/edgejs/commit/f1999f45d43453d508db45b3b52e4115983e26a8) Removed hacks: loopback, and spawn +- Sadhbh: edgejs [27118329](https://github.com/wasmerio/edgejs/commit/27118329f47b9876c3f415bf42669f9cc4a6568b) Fixes around edge process wrap, plus w/a for edge -e eval +- Sadhbh: edgejs [61ba9604](https://github.com/wasmerio/edgejs/commit/61ba9604327a7326a555c2d537d7214421c60a9c) various fixes diff --git a/plans/wasix-plan/edgejs/04_openssl_error_mapping_and_version_capability.md b/plans/wasix-plan/edgejs/04_openssl_error_mapping_and_version_capability.md index e66da29c5..0abd09319 100644 --- a/plans/wasix-plan/edgejs/04_openssl_error_mapping_and_version_capability.md +++ b/plans/wasix-plan/edgejs/04_openssl_error_mapping_and_version_capability.md @@ -107,5 +107,5 @@ which OpenSSL stack entry should become a Node.js error. ### [wasmerio/edgejs#91: [WIP] Node tests using Edgejs WASIX QuickJS](https://github.com/wasmerio/edgejs/pull/91) -- edgejs [5254f803](https://github.com/wasmerio/edgejs/commit/5254f803ff93200d79231d87501f813eb1b35147) OpenSSL update to 3.5 -- edgejs [b9ef182f](https://github.com/wasmerio/edgejs/commit/b9ef182f2322eb0e111f40427e4685852ec6a241) Fixes for OpenSSL error code mapping +- Sadhbh: edgejs [5254f803](https://github.com/wasmerio/edgejs/commit/5254f803ff93200d79231d87501f813eb1b35147) OpenSSL update to 3.5 +- Sadhbh: edgejs [b9ef182f](https://github.com/wasmerio/edgejs/commit/b9ef182f2322eb0e111f40427e4685852ec6a241) Fixes for OpenSSL error code mapping diff --git a/plans/wasix-plan/edgejs/05_runner_determinism_and_workflow_setup.md b/plans/wasix-plan/edgejs/05_runner_determinism_and_workflow_setup.md index ff4d48a2b..00202f8f6 100644 --- a/plans/wasix-plan/edgejs/05_runner_determinism_and_workflow_setup.md +++ b/plans/wasix-plan/edgejs/05_runner_determinism_and_workflow_setup.md @@ -98,10 +98,10 @@ This is an EdgeJS-owned test-infra plan. It should not change runtime semantics. ### [wasmerio/edgejs#91: [WIP] Node tests using Edgejs WASIX QuickJS](https://github.com/wasmerio/edgejs/pull/91) -- edgejs [f87e6848](https://github.com/wasmerio/edgejs/commit/f87e68487996f12d447974d0cefb85c3e956e885) Added note test runner for wasix quickjs -- edgejs [2baeceea](https://github.com/wasmerio/edgejs/commit/2baeceeaba641eb8e5ad61a88c1caf5b75a24547) Added node test runner wasix quickjs to gh workflow -- edgejs [352c55f3](https://github.com/wasmerio/edgejs/commit/352c55f3cf2d2b1083c48eb4cf190b3b43859ce0) Set wasmer version to 7.2.0-alpha.3 for wasix tests -- edgejs [c63d7771](https://github.com/wasmerio/edgejs/commit/c63d77715b7f0d96ac1ef027cfa885ffcae229e2) Install wasmer 7.2.0-alpha.3 using curl and shell script. Disable (temporarily other workflows). -- edgejs [fbbb21f7](https://github.com/wasmerio/edgejs/commit/fbbb21f7500bfa0f80cd84a49e63e579c929bebe) Updated sysroot=v2026-05-26.1 wasixcc=v0.4.3 -- edgejs [566e59a5](https://github.com/wasmerio/edgejs/commit/566e59a5b5dc451c4970dcc095ed80d6ed9e3120) Force llvm runtime -- edgejs [9aefbbe2](https://github.com/wasmerio/edgejs/commit/9aefbbe29753d32a43dcd3b11b9cd8dbbcb99b30) Isolate mapped host directories for wasix node test runner +- Sadhbh: edgejs [f87e6848](https://github.com/wasmerio/edgejs/commit/f87e68487996f12d447974d0cefb85c3e956e885) Added note test runner for wasix quickjs +- Sadhbh: edgejs [2baeceea](https://github.com/wasmerio/edgejs/commit/2baeceeaba641eb8e5ad61a88c1caf5b75a24547) Added node test runner wasix quickjs to gh workflow +- Sadhbh: edgejs [352c55f3](https://github.com/wasmerio/edgejs/commit/352c55f3cf2d2b1083c48eb4cf190b3b43859ce0) Set wasmer version to 7.2.0-alpha.3 for wasix tests +- Sadhbh: edgejs [c63d7771](https://github.com/wasmerio/edgejs/commit/c63d77715b7f0d96ac1ef027cfa885ffcae229e2) Install wasmer 7.2.0-alpha.3 using curl and shell script. Disable (temporarily other workflows). +- Sadhbh: edgejs [fbbb21f7](https://github.com/wasmerio/edgejs/commit/fbbb21f7500bfa0f80cd84a49e63e579c929bebe) Updated sysroot=v2026-05-26.1 wasixcc=v0.4.3 +- Sadhbh: edgejs [566e59a5](https://github.com/wasmerio/edgejs/commit/566e59a5b5dc451c4970dcc095ed80d6ed9e3120) Force llvm runtime +- Sadhbh: edgejs [9aefbbe2](https://github.com/wasmerio/edgejs/commit/9aefbbe29753d32a43dcd3b11b9cd8dbbcb99b30) Isolate mapped host directories for wasix node test runner diff --git a/plans/wasix-plan/libuv-wasix/01_use_posix_spawn_on_wasix.md b/plans/wasix-plan/libuv-wasix/01_use_posix_spawn_on_wasix.md index 4321c9bed..4eccac0ea 100644 --- a/plans/wasix-plan/libuv-wasix/01_use_posix_spawn_on_wasix.md +++ b/plans/wasix-plan/libuv-wasix/01_use_posix_spawn_on_wasix.md @@ -10,6 +10,13 @@ exit event. This occurs whenever Node uses `child_process.spawn()`, `execFile()`, `exec()`, `fork()`, or cluster worker creation on WASIX. +Branch context: + +`libuv-wasix` uses `ubi` as its mainline branch. In the local history, `ubi` +points at Martin's `ea792e22` "disable fork" baseline. The spawn proposal is +the next commit on `fix/spawn`: Artem's `ba06698b`, which replaces the disabled +fork path with WASIX `posix_spawn` / `proc_join` behavior. + Minimal Example: ```c @@ -80,8 +87,7 @@ Node child_process.spawn() The minimal libuv piece is just translation from libuv's process options and stdio container into `posix_spawn_file_actions_*` calls plus `posix_spawnp()`. -## Proposed Solution References - -### Commits without PR +## Related Commits -- libuv-wasix [ba06698b](https://github.com/Anodized-Titanium/libuv-wasix/commit/ba06698ba3f3b508a7a4eeacb23e0c911ebf4576) libuv-wasix: use WASIX posix_spawn/proc_join for child processes +- Martin: libuv-wasix [ea792e22](https://github.com/wasix-org/libuv/commit/ea792e22c8e88149d8279883ab4c7fd1fd716392) disable fork +- Artem: libuv-wasix [ba06698b](https://github.com/Anodized-Titanium/libuv-wasix/commit/ba06698ba3f3b508a7a4eeacb23e0c911ebf4576) libuv-wasix: use WASIX posix_spawn/proc_join for child processes diff --git a/plans/wasix-plan/libuv-wasix/02_udp_disconnect_and_multicast_option_compatibility.md b/plans/wasix-plan/libuv-wasix/02_udp_disconnect_and_multicast_option_compatibility.md index e9de2244b..274b89054 100644 --- a/plans/wasix-plan/libuv-wasix/02_udp_disconnect_and_multicast_option_compatibility.md +++ b/plans/wasix-plan/libuv-wasix/02_udp_disconnect_and_multicast_option_compatibility.md @@ -12,6 +12,18 @@ reach the behavior they are trying to assert. This occurs in connected UDP, default-host send, multicast TTL/interface/loop, and cluster/dgram tests. +Branch context: + +`libuv-wasix` uses `ubi` as its mainline branch. In the local history, the UDP +and multicast work is Sadhbh's `0ae770b9` on top of Artem's `ba06698b` spawn +commit, with Martin's `ea792e22` `ubi` commit as the baseline: + +```text +Sadhbh 0ae770b9 multicast TTL/loop/interface shims for WASIX, plus WASIX UDP disconnect avoids the unsupported AF_UNSPEC connect() trick. WASIX connected-state checks now trust libuv's handle flag. +Artem ba06698b libuv-wasix: use WASIX posix_spawn/proc_join for child processes +Martin ea792e22 disable fork # origin/ubi, ubi baseline +``` + Minimal Example: ```c @@ -94,4 +106,4 @@ multicast implementation remains a Wasmer task. ### Commits without PR -- libuv-wasix [0ae770b9](https://github.com/Anodized-Titanium/libuv-wasix/commit/0ae770b9daae35d97d3c9a2693a5b22362124f12) multicast TTL/loop/interface shims and UDP disconnect handling +- Sadhbh: libuv-wasix [0ae770b9](https://github.com/Anodized-Titanium/libuv-wasix/commit/0ae770b9daae35d97d3c9a2693a5b22362124f12) multicast TTL/loop/interface shims and UDP disconnect handling diff --git a/plans/wasix-plan/libuv-wasix/03_tcp_keepalive_timing_options.md b/plans/wasix-plan/libuv-wasix/03_tcp_keepalive_timing_options.md index e7d875d6d..599bf26cd 100644 --- a/plans/wasix-plan/libuv-wasix/03_tcp_keepalive_timing_options.md +++ b/plans/wasix-plan/libuv-wasix/03_tcp_keepalive_timing_options.md @@ -77,8 +77,8 @@ claiming that WASIX already implements every TCP timing option. ### [wasmerio/edgejs#91: [WIP] Node tests using Edgejs WASIX QuickJS](https://github.com/wasmerio/edgejs/pull/91) -- edgejs [9fa61f18](https://github.com/wasmerio/edgejs/commit/9fa61f1888f34c0785f54da8dd99ae193e536439) UV keep alive fix (disable unsupported options) +- Sadhbh: edgejs [9fa61f18](https://github.com/wasmerio/edgejs/commit/9fa61f1888f34c0785f54da8dd99ae193e536439) UV keep alive fix (disable unsupported options) ### Commits without PR -- libuv-wasix [8d537440](https://github.com/Anodized-Titanium/libuv-wasix/commit/8d537440533cfc290e33c7bcbf181ab414dd1850) Wasix-LibC supports SOL_SOCKET + SO_KEEPALIVE, however does not support additional options such as TCP_KEEPIDLE, TCP_KEEPINTVL, or TCP_KEEPCNT - so we disable them +- Sadhbh: libuv-wasix [8d537440](https://github.com/Anodized-Titanium/libuv-wasix/commit/8d537440533cfc290e33c7bcbf181ab414dd1850) Wasix-LibC supports SOL_SOCKET + SO_KEEPALIVE, however does not support additional options such as TCP_KEEPIDLE, TCP_KEEPINTVL, or TCP_KEEPCNT - so we disable them diff --git a/plans/wasix-plan/libuv-wasix/README.md b/plans/wasix-plan/libuv-wasix/README.md index 3ba90fcd4..1c149ec33 100644 --- a/plans/wasix-plan/libuv-wasix/README.md +++ b/plans/wasix-plan/libuv-wasix/README.md @@ -3,3 +3,14 @@ - [Use POSIX Spawn on WASIX](01_use_posix_spawn_on_wasix.md) - [UDP Disconnect and Multicast Option Compatibility](02_udp_disconnect_and_multicast_option_compatibility.md) - [TCP Keepalive Timing Options](03_tcp_keepalive_timing_options.md) + +## Branch Context + +For `libuv-wasix`, the mainline branch is named `ubi`, not `main`. The local +`fix/spawn` branch is currently layered as: + +```text +Sadhbh 0ae770b9 multicast TTL/loop/interface shims for WASIX, plus WASIX UDP disconnect avoids the unsupported AF_UNSPEC connect() trick. WASIX connected-state checks now trust libuv's handle flag. +Artem ba06698b libuv-wasix: use WASIX posix_spawn/proc_join for child processes +Martin ea792e22 disable fork # origin/ubi, ubi baseline +``` diff --git a/plans/wasix-plan/napi/01_quickjs_structured_clone_serdes_for_sharedarraybuffer.md b/plans/wasix-plan/napi/01_quickjs_structured_clone_serdes_for_sharedarraybuffer.md index 8d9163a70..39cfef5a3 100644 --- a/plans/wasix-plan/napi/01_quickjs_structured_clone_serdes_for_sharedarraybuffer.md +++ b/plans/wasix-plan/napi/01_quickjs_structured_clone_serdes_for_sharedarraybuffer.md @@ -90,8 +90,8 @@ bridges QuickJS SAB lifetime with Node/N-API ownership. ### [wasmerio/edgejs#91: [WIP] Node tests using Edgejs WASIX QuickJS](https://github.com/wasmerio/edgejs/pull/91) -- edgejs [38e01f79](https://github.com/wasmerio/edgejs/commit/38e01f79c15636badbab17faf6c31eef7d16abc6) Napi/Qjs: SharedArrayBuffer. Ssl certs in wasix tests. +- Sadhbh: edgejs [38e01f79](https://github.com/wasmerio/edgejs/commit/38e01f79c15636badbab17faf6c31eef7d16abc6) Napi/Qjs: SharedArrayBuffer. Ssl certs in wasix tests. ### Commits without PR -- napi [21f22e3](https://github.com/wasmerio/napi/commit/21f22e3cc3b90a2c453d2e4115e0db42c5d8f68a) QJS: Serdes for SharedArrayBuffer +- Sadhbh: napi [21f22e3](https://github.com/wasmerio/napi/commit/21f22e3cc3b90a2c453d2e4115e0db42c5d8f68a) QJS: Serdes for SharedArrayBuffer diff --git a/plans/wasix-plan/quickjs/01_wasi_stack_limit_should_match_non_wasi_quickjs.md b/plans/wasix-plan/quickjs/01_wasi_stack_limit_should_match_non_wasi_quickjs.md index 699391a16..6f5a327b8 100644 --- a/plans/wasix-plan/quickjs/01_wasi_stack_limit_should_match_non_wasi_quickjs.md +++ b/plans/wasix-plan/quickjs/01_wasi_stack_limit_should_match_non_wasi_quickjs.md @@ -85,4 +85,4 @@ shape of JavaScript stack overflow errors. ### Commits without PR -- quickjs [9a59f17](https://github.com/wasmerio/quickjs/commit/9a59f17a026ebc3b6932afa886c21ebf02689a2e) Set WASI stack limit same as non-WASI +- Sadhbh: quickjs [9a59f17](https://github.com/wasmerio/quickjs/commit/9a59f17a026ebc3b6932afa886c21ebf02689a2e) Set WASI stack limit same as non-WASI diff --git a/plans/wasix-plan/wasix-libc/01_socket_type_flags_sock_nonblock_and_sock_cloexec.md b/plans/wasix-plan/wasix-libc/01_socket_type_flags_sock_nonblock_and_sock_cloexec.md index 061b89d82..9e71ada61 100644 --- a/plans/wasix-plan/wasix-libc/01_socket_type_flags_sock_nonblock_and_sock_cloexec.md +++ b/plans/wasix-plan/wasix-libc/01_socket_type_flags_sock_nonblock_and_sock_cloexec.md @@ -144,4 +144,4 @@ int socket(int domain, int type, int protocol) { ### [wasix-org/wasix-libc#116: Fix socket options and last error](https://github.com/wasix-org/wasix-libc/pull/116) -- wasix-libc [c0853db](https://github.com/Anodized-Titanium/wasix-libc/commit/c0853db552fc0b85a23cdbe35a4f1d5b37c3cb8a) Open sockets with nonblock|cloexec flags correctly +- Sadhbh: wasix-libc [c0853db](https://github.com/Anodized-Titanium/wasix-libc/commit/c0853db552fc0b85a23cdbe35a4f1d5b37c3cb8a) Open sockets with nonblock|cloexec flags correctly diff --git a/plans/wasix-plan/wasix-libc/02_boolean_socket_option_pointer_handling.md b/plans/wasix-plan/wasix-libc/02_boolean_socket_option_pointer_handling.md index 90c72032f..3113f1d76 100644 --- a/plans/wasix-plan/wasix-libc/02_boolean_socket_option_pointer_handling.md +++ b/plans/wasix-plan/wasix-libc/02_boolean_socket_option_pointer_handling.md @@ -124,4 +124,4 @@ return __wasi_sock_set_opt_flag(fd, level, optname, enabled != 0); ### [wasix-org/wasix-libc#116: Fix socket options and last error](https://github.com/wasix-org/wasix-libc/pull/116) -- wasix-libc [17e686c](https://github.com/Anodized-Titanium/wasix-libc/commit/17e686c6a1cd6f11b3085aa23a42c0263fe99de5) Socket option incorrectly deferenced pointer +- Sadhbh: wasix-libc [17e686c](https://github.com/Anodized-Titanium/wasix-libc/commit/17e686c6a1cd6f11b3085aa23a42c0263fe99de5) Socket option incorrectly deferenced pointer diff --git a/plans/wasix-plan/wasix-libc/03_getsockopt_so_error.md b/plans/wasix-plan/wasix-libc/03_getsockopt_so_error.md index ec6b00752..6021788f8 100644 --- a/plans/wasix-plan/wasix-libc/03_getsockopt_so_error.md +++ b/plans/wasix-plan/wasix-libc/03_getsockopt_so_error.md @@ -117,8 +117,8 @@ C getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &len) ### [wasix-org/wasix-libc#116: Fix socket options and last error](https://github.com/wasix-org/wasix-libc/pull/116) -- wasix-libc [13626ca](https://github.com/Anodized-Titanium/wasix-libc/commit/13626cacc589cd284f97bd724715fb0184e2bc38) Last socket error +- Sadhbh: wasix-libc [13626ca](https://github.com/Anodized-Titanium/wasix-libc/commit/13626cacc589cd284f97bd724715fb0184e2bc38) Last socket error ### [wasmerio/wasmer#6685: Udp datagram receive and last err](https://github.com/wasmerio/wasmer/pull/6685) -- wasmer [0780b11bb2a](https://github.com/Anodized-Titanium/wasmer/commit/0780b11bb2abc4bfd532695c5887b215f6efbed7) Last socket error +- Sadhbh: wasmer [0780b11bb2a](https://github.com/Anodized-Titanium/wasmer/commit/0780b11bb2abc4bfd532695c5887b215f6efbed7) Last socket error diff --git a/plans/wasix-plan/wasix-libc/04_posix_st_mode_for_files_and_directories.md b/plans/wasix-plan/wasix-libc/04_posix_st_mode_for_files_and_directories.md index 4d7a94cb4..d94548afc 100644 --- a/plans/wasix-plan/wasix-libc/04_posix_st_mode_for_files_and_directories.md +++ b/plans/wasix-plan/wasix-libc/04_posix_st_mode_for_files_and_directories.md @@ -109,4 +109,4 @@ st->st_mode = mode; ### Commits without PR -- wasix-libc [631ef5d5](https://github.com/Anodized-Titanium/wasix-libc/commit/631ef5d5289a56b584fd84e4296627879d8d5e5c) Fix dir and file flags +- Sadhbh: wasix-libc [631ef5d5](https://github.com/Anodized-Titanium/wasix-libc/commit/631ef5d5289a56b584fd84e4296627879d8d5e5c) Fix dir and file flags diff --git a/plans/wasix-plan/wasix-libc/05_loopback_reverse_lookup.md b/plans/wasix-plan/wasix-libc/05_loopback_reverse_lookup.md index d531dc39a..7465cec92 100644 --- a/plans/wasix-plan/wasix-libc/05_loopback_reverse_lookup.md +++ b/plans/wasix-plan/wasix-libc/05_loopback_reverse_lookup.md @@ -102,4 +102,4 @@ case AF_INET6: ### Commits without PR -- wasix-libc [1a028f0e9](https://github.com/Anodized-Titanium/wasix-libc/commit/1a028f0e9aeaffa580fc6bc5962069531fda322d) Reverse loopback +- Sadhbh: wasix-libc [1a028f0e9](https://github.com/Anodized-Titanium/wasix-libc/commit/1a028f0e9aeaffa580fc6bc5962069531fda322d) Reverse loopback diff --git a/plans/wasix-plan/wasmer/01_udp_datagram_receive_readiness_and_backlog.md b/plans/wasix-plan/wasmer/01_udp_datagram_receive_readiness_and_backlog.md index 131d61b32..5387e304d 100644 --- a/plans/wasix-plan/wasmer/01_udp_datagram_receive_readiness_and_backlog.md +++ b/plans/wasix-plan/wasmer/01_udp_datagram_receive_readiness_and_backlog.md @@ -191,8 +191,8 @@ guest buffer. ### [wasmerio/wasmer#6685: Udp datagram receive and last err](https://github.com/wasmerio/wasmer/pull/6685) -- wasmer [c56897f3bec](https://github.com/Anodized-Titanium/wasmer/commit/c56897f3beced2c9c4861137bf81f32320a14b7c) Fixed UDP socket dgram receive -- wasmer [65e0eb571ef](https://github.com/Anodized-Titanium/wasmer/commit/65e0eb571ef4fde1aee765fae2955956dcbd1d8f) Peek UDP packets correctly -- wasmer [66552d27be9](https://github.com/Anodized-Titanium/wasmer/commit/66552d27be9a7efc7962aab22bd60375476c07bb) Fix merge conflict mistake -- wasmer [8e92612e917](https://github.com/Anodized-Titanium/wasmer/commit/8e92612e917cb9745f21aab2634dd7687c229d33) Conflict resolution for UDP receive/readiness changes in `lib/virtual-net/src/{client,host,lib}.rs` and `lib/wasix/src/net/socket.rs` +- Sadhbh: wasmer [c56897f3bec](https://github.com/Anodized-Titanium/wasmer/commit/c56897f3beced2c9c4861137bf81f32320a14b7c) Fixed UDP socket dgram receive +- Sadhbh: wasmer [65e0eb571ef](https://github.com/Anodized-Titanium/wasmer/commit/65e0eb571ef4fde1aee765fae2955956dcbd1d8f) Peek UDP packets correctly +- Sadhbh: wasmer [66552d27be9](https://github.com/Anodized-Titanium/wasmer/commit/66552d27be9a7efc7962aab22bd60375476c07bb) Fix merge conflict mistake +- Sadhbh: wasmer [8e92612e917](https://github.com/Anodized-Titanium/wasmer/commit/8e92612e917cb9745f21aab2634dd7687c229d33) Conflict resolution for UDP receive/readiness changes in `lib/virtual-net/src/{client,host,lib}.rs` and `lib/wasix/src/net/socket.rs` diff --git a/plans/wasix-plan/wasmer/02_connected_udp_send_peer_state_and_emsgsize.md b/plans/wasix-plan/wasmer/02_connected_udp_send_peer_state_and_emsgsize.md index c442cd8b9..de5dba426 100644 --- a/plans/wasix-plan/wasmer/02_connected_udp_send_peer_state_and_emsgsize.md +++ b/plans/wasix-plan/wasmer/02_connected_udp_send_peer_state_and_emsgsize.md @@ -183,5 +183,5 @@ fn udp_connected_send(iovs: &[IoSlice<'_>], peer: SocketAddr) -> Result Errno { ### [wasix-org/wasix-libc#116: Fix socket options and last error](https://github.com/wasix-org/wasix-libc/pull/116) -- wasix-libc [13626ca](https://github.com/Anodized-Titanium/wasix-libc/commit/13626cacc589cd284f97bd724715fb0184e2bc38) Last socket error +- Sadhbh: wasix-libc [13626ca](https://github.com/Anodized-Titanium/wasix-libc/commit/13626cacc589cd284f97bd724715fb0184e2bc38) Last socket error ### [wasmerio/wasmer#6685: Udp datagram receive and last err](https://github.com/wasmerio/wasmer/pull/6685) -- wasmer [0780b11bb2a](https://github.com/Anodized-Titanium/wasmer/commit/0780b11bb2abc4bfd532695c5887b215f6efbed7) Last socket error +- Sadhbh: wasmer [0780b11bb2a](https://github.com/Anodized-Titanium/wasmer/commit/0780b11bb2abc4bfd532695c5887b215f6efbed7) Last socket error diff --git a/plans/wasix-plan/wasmer/04_proc_spawn3_proc_exec4_for_real_argv_envp.md b/plans/wasix-plan/wasmer/04_proc_spawn3_proc_exec4_for_real_argv_envp.md index b77bf2c86..9024dc25a 100644 --- a/plans/wasix-plan/wasmer/04_proc_spawn3_proc_exec4_for_real_argv_envp.md +++ b/plans/wasix-plan/wasmer/04_proc_spawn3_proc_exec4_for_real_argv_envp.md @@ -134,8 +134,11 @@ spawn_process(path, argv, envp, file_actions) ### Commits without PR -- wasmer [d80c3d8c980](https://github.com/Anodized-Titanium/wasmer/commit/d80c3d8c98065562c845c2ec8acfe0668798a166) Add proc_spawn3 and proc_exec4... -- wasmer [b5880b5268b](https://github.com/Anodized-Titanium/wasmer/commit/b5880b5268b74aefc5b4295767286976d83b0deb) apply review comments -- wasmer [e9d1478b914](https://github.com/Anodized-Titanium/wasmer/commit/e9d1478b914c224051541d35281ff0a0638fab21) fix tests - wasix-libc proc_spawn3/proc_exec4 lowering -- libuv-wasix [ba06698b](https://github.com/Anodized-Titanium/libuv-wasix/commit/ba06698ba3f3b508a7a4eeacb23e0c911ebf4576) libuv-wasix: use WASIX posix_spawn/proc_join... + +## Related Commits + +- Arshia: wasmer [d80c3d8c980](https://github.com/Anodized-Titanium/wasmer/commit/d80c3d8c98065562c845c2ec8acfe0668798a166) Add proc_spawn3 and proc_exec4... +- Arshia: wasmer [b5880b5268b](https://github.com/Anodized-Titanium/wasmer/commit/b5880b5268b74aefc5b4295767286976d83b0deb) apply review comments +- Arshia: wasmer [e9d1478b914](https://github.com/Anodized-Titanium/wasmer/commit/e9d1478b914c224051541d35281ff0a0638fab21) fix tests +- Artem: libuv-wasix [ba06698b](https://github.com/Anodized-Titanium/libuv-wasix/commit/ba06698ba3f3b508a7a4eeacb23e0c911ebf4576) libuv-wasix: use WASIX posix_spawn/proc_join... diff --git a/plans/wasix-plan/wasmer/05_partial_success_for_fd_write.md b/plans/wasix-plan/wasmer/05_partial_success_for_fd_write.md index 1a3e0493c..7e19e8072 100644 --- a/plans/wasix-plan/wasmer/05_partial_success_for_fd_write.md +++ b/plans/wasix-plan/wasmer/05_partial_success_for_fd_write.md @@ -101,4 +101,4 @@ Ok(written) ### [wasmerio/wasmer#6685: Udp datagram receive and last err](https://github.com/wasmerio/wasmer/pull/6685) -- wasmer [945aac9b18f](https://github.com/Anodized-Titanium/wasmer/commit/945aac9b18fafbbee9b5605e6f57211dd94afaf8) fd_write should not fail it there was at least one success +- Sadhbh: wasmer [945aac9b18f](https://github.com/Anodized-Titanium/wasmer/commit/945aac9b18fafbbee9b5605e6f57211dd94afaf8) fd_write should not fail it there was at least one success From 5261cf15014d439267595d599e60eba8be3f5ce9 Mon Sep 17 00:00:00 2001 From: Sadhbh Code Date: Mon, 15 Jun 2026 15:25:32 +0100 Subject: [PATCH 32/65] Updated index pageg of the wasix test plans --- plans/wasix-plan/index.md | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/plans/wasix-plan/index.md b/plans/wasix-plan/index.md index c51ccecf5..5d30ba75d 100644 --- a/plans/wasix-plan/index.md +++ b/plans/wasix-plan/index.md @@ -15,20 +15,13 @@ This plan is written as if the tree starts from the GitHub-style baseline: - Wasmer is on `main`. - `wasix-libc` is on `main`. - QuickJS is on `master`. -- `libuv-wasix` is on its baseline branch. -- EdgeJS is on the current WASIX QuickJS test branch. +- `libuv-wasix` is on its `ubi` branch. +- EdgeJS is on `main` branch. The commit hashes in headings are reference points only. Treat them as prior art for the shape and scope of the change, not as statements that the baseline already contains the behavior. -The source comparison document is: - -```text -~/src/wasmer-workspace/docs/edgejs/006_acoose_vs_dunlewy_recovered_tests.md -``` - - ## Cross-Project Rule Fix behavior at the lowest POSIX-compatible layer that owns it: From d42ae2ef0e35996da7c634c1528705b567ce6a09 Mon Sep 17 00:00:00 2001 From: Sadhbh Code Date: Mon, 15 Jun 2026 16:54:46 +0100 Subject: [PATCH 33/65] Update in wasix test plans about reverted commits --- ...uest_workarounds_after_lower_layer_plans_land.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/plans/wasix-plan/edgejs/03_remove_wasix_only_guest_workarounds_after_lower_layer_plans_land.md b/plans/wasix-plan/edgejs/03_remove_wasix_only_guest_workarounds_after_lower_layer_plans_land.md index eb50c973c..2a0f5d92f 100644 --- a/plans/wasix-plan/edgejs/03_remove_wasix_only_guest_workarounds_after_lower_layer_plans_land.md +++ b/plans/wasix-plan/edgejs/03_remove_wasix_only_guest_workarounds_after_lower_layer_plans_land.md @@ -120,10 +120,19 @@ The rule is simple: if a JS test passes natively and only fails on WASIX because of libc/runtime semantics, prefer fixing libc/runtime and deleting the EdgeJS branch. +Commit relationship: + +`27118329` added transitional EdgeJS-side process-wrapper and multiline `-e` +workarounds while the WASIX process ABI still flattened argv through newline +delimited strings. Those changes were useful for proving the failure cause, but +they were not the desired final compatibility layer. `f1999f45` then removes / +reverts those guest-side loopback and spawn hacks after the lower-layer +`wasix-libc` / Wasmer process and resolver fixes provide POSIX-like behavior. + ## Proposed Solution References ### [wasmerio/edgejs#91: [WIP] Node tests using Edgejs WASIX QuickJS](https://github.com/wasmerio/edgejs/pull/91) -- Sadhbh: edgejs [f1999f45](https://github.com/wasmerio/edgejs/commit/f1999f45d43453d508db45b3b52e4115983e26a8) Removed hacks: loopback, and spawn -- Sadhbh: edgejs [27118329](https://github.com/wasmerio/edgejs/commit/27118329f47b9876c3f415bf42669f9cc4a6568b) Fixes around edge process wrap, plus w/a for edge -e eval +- Sadhbh: edgejs [27118329](https://github.com/wasmerio/edgejs/commit/27118329f47b9876c3f415bf42669f9cc4a6568b) Transitional process-wrapper fixes and temporary EdgeJS `-e` eval workaround. PR view: [wasmerio/edgejs#91 @ 27118329](https://github.com/wasmerio/edgejs/pull/91/changes/27118329f47b9876c3f415bf42669f9cc4a6568b) +- Sadhbh: edgejs [f1999f45](https://github.com/wasmerio/edgejs/commit/f1999f45d43453d508db45b3b52e4115983e26a8) Removes / reverts the previous EdgeJS-only loopback and spawn hacks once the fixes belong in lower layers. PR view: [wasmerio/edgejs#91 @ f1999f45](https://github.com/wasmerio/edgejs/pull/91/changes/f1999f45d43453d508db45b3b52e4115983e26a8) - Sadhbh: edgejs [61ba9604](https://github.com/wasmerio/edgejs/commit/61ba9604327a7326a555c2d537d7214421c60a9c) various fixes From 158bd4fbe3d8e9fcdee15266f75915abcf608289 Mon Sep 17 00:00:00 2001 From: Sadhbh Code Date: Mon, 15 Jun 2026 17:05:59 +0100 Subject: [PATCH 34/65] Update in wasix test plans about reverted commits corrected by hand --- ...workarounds_after_lower_layer_plans_land.md | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/plans/wasix-plan/edgejs/03_remove_wasix_only_guest_workarounds_after_lower_layer_plans_land.md b/plans/wasix-plan/edgejs/03_remove_wasix_only_guest_workarounds_after_lower_layer_plans_land.md index 2a0f5d92f..71547544d 100644 --- a/plans/wasix-plan/edgejs/03_remove_wasix_only_guest_workarounds_after_lower_layer_plans_land.md +++ b/plans/wasix-plan/edgejs/03_remove_wasix_only_guest_workarounds_after_lower_layer_plans_land.md @@ -120,19 +120,13 @@ The rule is simple: if a JS test passes natively and only fails on WASIX because of libc/runtime semantics, prefer fixing libc/runtime and deleting the EdgeJS branch. -Commit relationship: - -`27118329` added transitional EdgeJS-side process-wrapper and multiline `-e` -workarounds while the WASIX process ABI still flattened argv through newline -delimited strings. Those changes were useful for proving the failure cause, but -they were not the desired final compatibility layer. `f1999f45` then removes / -reverts those guest-side loopback and spawn hacks after the lower-layer -`wasix-libc` / Wasmer process and resolver fixes provide POSIX-like behavior. - ## Proposed Solution References ### [wasmerio/edgejs#91: [WIP] Node tests using Edgejs WASIX QuickJS](https://github.com/wasmerio/edgejs/pull/91) -- Sadhbh: edgejs [27118329](https://github.com/wasmerio/edgejs/commit/27118329f47b9876c3f415bf42669f9cc4a6568b) Transitional process-wrapper fixes and temporary EdgeJS `-e` eval workaround. PR view: [wasmerio/edgejs#91 @ 27118329](https://github.com/wasmerio/edgejs/pull/91/changes/27118329f47b9876c3f415bf42669f9cc4a6568b) -- Sadhbh: edgejs [f1999f45](https://github.com/wasmerio/edgejs/commit/f1999f45d43453d508db45b3b52e4115983e26a8) Removes / reverts the previous EdgeJS-only loopback and spawn hacks once the fixes belong in lower layers. PR view: [wasmerio/edgejs#91 @ f1999f45](https://github.com/wasmerio/edgejs/pull/91/changes/f1999f45d43453d508db45b3b52e4115983e26a8) -- Sadhbh: edgejs [61ba9604](https://github.com/wasmerio/edgejs/commit/61ba9604327a7326a555c2d537d7214421c60a9c) various fixes +- Sadhbh: edgejs [f1999f45](https://github.com/wasmerio/edgejs/commit/f1999f45d43453d508db45b3b52e4115983e26a8) Removed hacks: loopback, and spawn +- Sadhbh: edgejs [27118329](https://github.com/wasmerio/edgejs/commit/27118329f47b9876c3f415bf42669f9cc4a6568b) Fixes around edge process wrap, plus w/a for edge -e eval *(reverted by next commits)* +- Sadhbh: edgejs [61ba9604](https://github.com/wasmerio/edgejs/commit/61ba9604327a7326a555c2d537d7214421c60a9c) various fixes *(reverted by next commits)* + +- Sadhbh edgejs [27118329](https://github.com/wasmerio/edgejs/commit/27118329f47b9876c3f415bf42669f9cc4a6568b) Fixes around edge process wrap, plus w/a for edge -e eval +- Sadhbh edgejs [f1999f45](https://github.com/wasmerio/edgejs/pull/91/changes/f1999f45d43453d508db45b3b52e4115983e26a8) various fixes From a31a677125eeee6b19b03ae557bfd9e4a01ab3b5 Mon Sep 17 00:00:00 2001 From: Sadhbh Code Date: Mon, 15 Jun 2026 17:13:42 +0100 Subject: [PATCH 35/65] Update in wasix test plans about reverted commits corrected again --- ..._only_guest_workarounds_after_lower_layer_plans_land.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/plans/wasix-plan/edgejs/03_remove_wasix_only_guest_workarounds_after_lower_layer_plans_land.md b/plans/wasix-plan/edgejs/03_remove_wasix_only_guest_workarounds_after_lower_layer_plans_land.md index 71547544d..b8ff4d95f 100644 --- a/plans/wasix-plan/edgejs/03_remove_wasix_only_guest_workarounds_after_lower_layer_plans_land.md +++ b/plans/wasix-plan/edgejs/03_remove_wasix_only_guest_workarounds_after_lower_layer_plans_land.md @@ -125,8 +125,5 @@ branch. ### [wasmerio/edgejs#91: [WIP] Node tests using Edgejs WASIX QuickJS](https://github.com/wasmerio/edgejs/pull/91) - Sadhbh: edgejs [f1999f45](https://github.com/wasmerio/edgejs/commit/f1999f45d43453d508db45b3b52e4115983e26a8) Removed hacks: loopback, and spawn -- Sadhbh: edgejs [27118329](https://github.com/wasmerio/edgejs/commit/27118329f47b9876c3f415bf42669f9cc4a6568b) Fixes around edge process wrap, plus w/a for edge -e eval *(reverted by next commits)* -- Sadhbh: edgejs [61ba9604](https://github.com/wasmerio/edgejs/commit/61ba9604327a7326a555c2d537d7214421c60a9c) various fixes *(reverted by next commits)* - -- Sadhbh edgejs [27118329](https://github.com/wasmerio/edgejs/commit/27118329f47b9876c3f415bf42669f9cc4a6568b) Fixes around edge process wrap, plus w/a for edge -e eval -- Sadhbh edgejs [f1999f45](https://github.com/wasmerio/edgejs/pull/91/changes/f1999f45d43453d508db45b3b52e4115983e26a8) various fixes +- Sadhbh: edgejs [27118329](https://github.com/wasmerio/edgejs/commit/27118329f47b9876c3f415bf42669f9cc4a6568b) Fixes around edge process wrap, plus w/a for edge -e eval *(reverted by f1999f45 above)* +- Sadhbh: edgejs [61ba9604](https://github.com/wasmerio/edgejs/commit/61ba9604327a7326a555c2d537d7214421c60a9c) various fixes *(reverted by 27118329 above)* From e3a20c38a5bbe4c12ff04f697d13871cb07eefde Mon Sep 17 00:00:00 2001 From: Sadhbh Code Date: Mon, 15 Jun 2026 17:56:26 +0100 Subject: [PATCH 36/65] Removed not needed code --- .../libuv-wasix/04_child_stdio_pipe_isatty.md | 224 ++++++++++++++++++ plans/wasix-plan/libuv-wasix/README.md | 1 + src/edge_util.cc | 8 - src/internal_binding/binding_crypto.cc | 20 +- 4 files changed, 229 insertions(+), 24 deletions(-) create mode 100644 plans/wasix-plan/libuv-wasix/04_child_stdio_pipe_isatty.md diff --git a/plans/wasix-plan/libuv-wasix/04_child_stdio_pipe_isatty.md b/plans/wasix-plan/libuv-wasix/04_child_stdio_pipe_isatty.md new file mode 100644 index 000000000..f273009d2 --- /dev/null +++ b/plans/wasix-plan/libuv-wasix/04_child_stdio_pipe_isatty.md @@ -0,0 +1,224 @@ +# libuv-wasix: child stdio pipe `isatty()` classification + +Why this is a problem: + +`uv_guess_handle(fd)` checks `isatty(fd)` before it falls back to `fstat()`. +That is correct on native Unix: a pipe-backed child stdio fd returns false from +`isatty()`, then `fstat()` exposes a FIFO/socket-like fd and libuv classifies it +as `UV_NAMED_PIPE`. + +On WASIX, `wasix-libc` currently implements `isatty()` from WASI fd metadata: +`CHARACTER_DEVICE` plus no seek/tell rights means TTY. That heuristic can be +wrong for child stdio fds that are pipe endpoints. If the runtime exposes a +spawned child stderr/stdout pipe as a non-seekable character device, `isatty()` +returns true and `uv_guess_handle()` stops early with `UV_TTY`. Node then uses +the TTY path for a fd that should behave like a pipe. + +This is not an EdgeJS behavior difference. Native EdgeJS passes because native +Linux/macOS kernels distinguish a terminal from a pipe at the fd level: + +```text +pipe-backed fd -> isatty(fd) == 0 -> fstat(fd) says FIFO/socket -> UV_NAMED_PIPE +real terminal -> isatty(fd) == 1 -> UV_TTY +``` + +The WASIX failure is that the lower layers do not preserve that distinction for +some child stdio pipe fds. + +When it occurs: + +- spawned children with stdout/stderr connected to parent-created pipes; +- Node/libuv code that calls `uv_guess_handle(0|1|2)` during bootstrap; +- stream tests such as `sequential/test-stream2-stderr-sync` where stderr must + be accepted as a pipe-like stream. + +Minimal Example: + +This C program spawns itself with the child's `stderr` duplicated from the write +end of a pipe. In the child, `isatty(STDERR_FILENO)` must be false. The parent +reads the child result from the pipe. + +```c +#include +#include +#include +#include +#include +#include +#include +#include + +extern char **environ; + +static int child(void) { + errno = 0; + int tty = isatty(STDERR_FILENO); + int saved_errno = errno; + + struct stat st; + memset(&st, 0, sizeof(st)); + int stat_rc = fstat(STDERR_FILENO, &st); + + dprintf(STDERR_FILENO, + "isatty=%d errno=%d stat_rc=%d fifo=%d chr=%d\n", + tty, + saved_errno, + stat_rc, + stat_rc == 0 && S_ISFIFO(st.st_mode), + stat_rc == 0 && S_ISCHR(st.st_mode)); + + return tty == 0 ? 0 : 1; +} + +int main(int argc, char **argv) { + if (argc == 2 && strcmp(argv[1], "--child") == 0) + return child(); + + int pipefd[2]; + if (pipe(pipefd) != 0) { + perror("pipe"); + return 1; + } + + posix_spawn_file_actions_t actions; + posix_spawn_file_actions_init(&actions); + posix_spawn_file_actions_adddup2(&actions, pipefd[1], STDERR_FILENO); + posix_spawn_file_actions_addclose(&actions, pipefd[0]); + posix_spawn_file_actions_addclose(&actions, pipefd[1]); + + char *child_argv[] = { argv[0], "--child", NULL }; + pid_t pid; + int rc = posix_spawnp(&pid, argv[0], &actions, NULL, child_argv, environ); + posix_spawn_file_actions_destroy(&actions); + close(pipefd[1]); + + if (rc != 0) { + errno = rc; + perror("posix_spawnp"); + close(pipefd[0]); + return 1; + } + + char buf[256]; + ssize_t nread = read(pipefd[0], buf, sizeof(buf) - 1); + if (nread < 0) { + perror("read"); + close(pipefd[0]); + return 1; + } + buf[nread] = '\0'; + close(pipefd[0]); + + int status = 0; + waitpid(pid, &status, 0); + + printf("child said: %s", buf); + return WIFEXITED(status) ? WEXITSTATUS(status) : 1; +} +``` + +Expected output shape on a POSIX-compatible runtime: + +```text +child said: isatty=0 errno=25 stat_rc=0 fifo=1 chr=0 +``` + +The exact `errno` after `isatty()` may vary, but the important part is +`isatty=0` for the pipe-backed child `stderr` fd. + +Callgraph and boundary: + +Current problematic path: + +```text +Node bootstrap / stream setup + -> EdgeJS guessHandleType(fd) + -> uv_guess_handle(fd) + -> isatty(fd) + -> wasix-libc __isatty(fd) + -> __wasi_fd_fdstat_get(fd) + -> CHARACTER_DEVICE + no seek/tell rights => true + HERE IS THE PROBLEM: a child stdio pipe can be classified as TTY before + libuv reaches fstat() and pipe/socket detection. + -> uv_guess_handle() returns UV_TTY + -> Node opens stderr/stdout through TTY behavior instead of pipe behavior +``` + +On native Linux, the problematic branch is not taken for a pipe: + +```text +uv_guess_handle(fd) + -> isatty(pipe_fd) == false + -> fstat(pipe_fd) says FIFO + -> UV_NAMED_PIPE +``` + +Proposed solution: + +Fix this below EdgeJS. EdgeJS should not need to rewrite `UV_TTY` into +`UV_NAMED_PIPE` for stdio fds. + +Preferred runtime/libc behavior: + +- Wasmer should preserve fd kind for child stdio pipe endpoints created by + `posix_spawn` / WASIX process file actions. A pipe endpoint must not be + exposed to libc as an indistinguishable TTY-like character device. +- `wasix-libc` `isatty(fd)` should become fd-specific rather than global or + heuristic-only. It should return true only when the runtime confirms that the + specific fd is a terminal. +- `wasix-libc` should reject pipe-like fds before the old + `CHARACTER_DEVICE && !SEEK && !TELL` fallback. If `fstat(fd)` can identify a + pipe/FIFO, `isatty(fd)` must return false. + +Possible implementation shape: + +```text +wasix-libc isatty(fd) + -> ask runtime whether this exact fd is a tty + -> true: return 1 + -> false for pipe/socket/file: errno = ENOTTY; return 0 + -> only use legacy fdstat heuristic if no fd-specific answer exists +``` + +If the existing `tty_get` ABI remains global and fd-less, add a fd-aware WASIX +query or expose enough fd metadata through `fdstat` / `filestat` for libc to +separate: + +```text +real tty fd +child stdio pipe fd +regular file fd +socket fd +``` + +Proposed callgraph after the fix: + +```text +EdgeJS guessHandleType(fd) + -> uv_guess_handle(fd) + -> isatty(child_stderr_pipe_fd) == false + -> fstat(child_stderr_pipe_fd) identifies pipe-like fd + -> uv_guess_handle() returns UV_NAMED_PIPE + -> Node stream bootstrap accepts stderr/stdout as pipe-like streams +``` + +Relevant code paths: + +```text +~/src/edgejs/deps/libuv-wasix/src/unix/tty.c + uv_guess_handle() order: isatty() first, fstat() second + +~/src/wasix-libc/libc-bottom-half/sources/isatty.c + current fdstat heuristic + +~/src/wasix-libc/libc-bottom-half/cloudlibc/src/libc/sys/ioctl/ioctl.c + current tty_get usage is not fd-specific + +~/src/wasmer/lib/wasix/src/syscalls/wasix/proc_spawn*.rs +~/src/wasmer/lib/wasix/src/syscalls/wasix/proc_exec*.rs + process file actions / child stdio fd setup + +~/src/wasmer/lib/wasix/src/state/builder.rs +~/src/wasmer/lib/virtual-fs/src/* + fd table and virtual file kind exposed to guest libc +``` diff --git a/plans/wasix-plan/libuv-wasix/README.md b/plans/wasix-plan/libuv-wasix/README.md index 1c149ec33..d92d9a7a3 100644 --- a/plans/wasix-plan/libuv-wasix/README.md +++ b/plans/wasix-plan/libuv-wasix/README.md @@ -3,6 +3,7 @@ - [Use POSIX Spawn on WASIX](01_use_posix_spawn_on_wasix.md) - [UDP Disconnect and Multicast Option Compatibility](02_udp_disconnect_and_multicast_option_compatibility.md) - [TCP Keepalive Timing Options](03_tcp_keepalive_timing_options.md) +- [Child Stdio Pipe `isatty()` Classification](04_child_stdio_pipe_isatty.md) ## Branch Context diff --git a/src/edge_util.cc b/src/edge_util.cc index 2f6e2aa34..fe27be431 100644 --- a/src/edge_util.cc +++ b/src/edge_util.cc @@ -503,14 +503,6 @@ napi_value GuessHandleType(napi_env env, napi_callback_info info) { return Undefined(env); } uv_handle_type t = uv_guess_handle(static_cast(fd)); -#ifdef __wasi__ - if (t == UV_TTY && fd >= 0 && fd <= 2) { - struct stat fd_stat {}; - if (fstat(fd, &fd_stat) == 0) { - t = UV_NAMED_PIPE; - } - } -#endif #ifndef _WIN32 if (fd == 0 && t == UV_FILE) { struct stat fd_stat {}; diff --git a/src/internal_binding/binding_crypto.cc b/src/internal_binding/binding_crypto.cc index 985c10869..0eac56378 100644 --- a/src/internal_binding/binding_crypto.cc +++ b/src/internal_binding/binding_crypto.cc @@ -728,22 +728,6 @@ void ThrowLastOpenSslMessage(napi_env env, const char* fallback_message) { napi_throw(env, CreateOpenSslError(env, MapOpenSslErrorCode(selected), selected, fallback_message)); } -unsigned long ConsumePreferredOpenSslError() { - unsigned long first = 0; - unsigned long selected = 0; - int selected_priority = 0; - unsigned long err = 0; - while ((err = ERR_get_error()) != 0) { - if (first == 0) first = err; - const int priority = OpenSslMappedErrorPriority(err, true); - if (priority > selected_priority) { - selected = err; - selected_priority = priority; - } - } - return selected != 0 ? selected : first; -} - unsigned long ConsumePreferredOpenSslKeyParseError(bool require_private) { unsigned long first = 0; unsigned long selected = 0; @@ -760,6 +744,10 @@ unsigned long ConsumePreferredOpenSslKeyParseError(bool require_private) { return selected != 0 ? selected : first; } +unsigned long ConsumePreferredOpenSslError() { + return ConsumePreferredOpenSslKeyParseError(true); +} + constexpr unsigned long kOpenSslDecoderUnsupportedError = 0x1E08010CUL; napi_value BuildJobResult(napi_env env, napi_value err, napi_value value); From bfeaf7fec8d0c34b92dca717ddaf59b98b6795d8 Mon Sep 17 00:00:00 2001 From: Sadhbh Code Date: Mon, 15 Jun 2026 18:11:20 +0100 Subject: [PATCH 37/65] Updated wasix test plan moved child stdio issue --- plans/wasix-plan/index.md | 1 + plans/wasix-plan/libuv-wasix/README.md | 1 - .../06_child_stdio_pipe_isatty.md} | 2 +- plans/wasix-plan/wasix-libc/README.md | 1 + 4 files changed, 3 insertions(+), 2 deletions(-) rename plans/wasix-plan/{libuv-wasix/04_child_stdio_pipe_isatty.md => wasix-libc/06_child_stdio_pipe_isatty.md} (99%) diff --git a/plans/wasix-plan/index.md b/plans/wasix-plan/index.md index 5d30ba75d..5d73e6ea4 100644 --- a/plans/wasix-plan/index.md +++ b/plans/wasix-plan/index.md @@ -63,6 +63,7 @@ socket modes, stat modes, or process behavior. - [`getsockopt(SO_ERROR)`](wasix-libc/03_getsockopt_so_error.md) - [POSIX `st_mode` for Files and Directories](wasix-libc/04_posix_st_mode_for_files_and_directories.md) - [Loopback Reverse Lookup](wasix-libc/05_loopback_reverse_lookup.md) +- [Child Stdio Pipe `isatty()` Classification](wasix-libc/06_child_stdio_pipe_isatty.md) ### libuv-wasix diff --git a/plans/wasix-plan/libuv-wasix/README.md b/plans/wasix-plan/libuv-wasix/README.md index d92d9a7a3..1c149ec33 100644 --- a/plans/wasix-plan/libuv-wasix/README.md +++ b/plans/wasix-plan/libuv-wasix/README.md @@ -3,7 +3,6 @@ - [Use POSIX Spawn on WASIX](01_use_posix_spawn_on_wasix.md) - [UDP Disconnect and Multicast Option Compatibility](02_udp_disconnect_and_multicast_option_compatibility.md) - [TCP Keepalive Timing Options](03_tcp_keepalive_timing_options.md) -- [Child Stdio Pipe `isatty()` Classification](04_child_stdio_pipe_isatty.md) ## Branch Context diff --git a/plans/wasix-plan/libuv-wasix/04_child_stdio_pipe_isatty.md b/plans/wasix-plan/wasix-libc/06_child_stdio_pipe_isatty.md similarity index 99% rename from plans/wasix-plan/libuv-wasix/04_child_stdio_pipe_isatty.md rename to plans/wasix-plan/wasix-libc/06_child_stdio_pipe_isatty.md index f273009d2..d22f2cacc 100644 --- a/plans/wasix-plan/libuv-wasix/04_child_stdio_pipe_isatty.md +++ b/plans/wasix-plan/wasix-libc/06_child_stdio_pipe_isatty.md @@ -1,4 +1,4 @@ -# libuv-wasix: child stdio pipe `isatty()` classification +# wasix-libc: child stdio pipe `isatty()` classification Why this is a problem: diff --git a/plans/wasix-plan/wasix-libc/README.md b/plans/wasix-plan/wasix-libc/README.md index 6ddfa23d1..eed041234 100644 --- a/plans/wasix-plan/wasix-libc/README.md +++ b/plans/wasix-plan/wasix-libc/README.md @@ -5,3 +5,4 @@ - [`getsockopt(SO_ERROR)`](03_getsockopt_so_error.md) - [POSIX `st_mode` for Files and Directories](04_posix_st_mode_for_files_and_directories.md) - [Loopback Reverse Lookup](05_loopback_reverse_lookup.md) +- [Child Stdio Pipe `isatty()` Classification](06_child_stdio_pipe_isatty.md) From 2f20b4e7b74f4a89f6def5bf8c7344b09939ab9f Mon Sep 17 00:00:00 2001 From: Sadhbh Code Date: Mon, 15 Jun 2026 18:52:35 +0100 Subject: [PATCH 38/65] Reverted accidental changes to lib --- lib/child_process.js | 1 - lib/net.js | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/child_process.js b/lib/child_process.js index 330e3cf11..17c6b69c1 100644 --- a/lib/child_process.js +++ b/lib/child_process.js @@ -564,7 +564,6 @@ function copyPermissionModelFlagsToEnv(env, key, args) { } let emittedDEP0190Already = false; - function normalizeSpawnArguments(file, args, options) { validateString(file, 'file'); validateArgumentNullCheck(file, 'file'); diff --git a/lib/net.js b/lib/net.js index d4fdda6dd..a391e9da3 100644 --- a/lib/net.js +++ b/lib/net.js @@ -427,7 +427,7 @@ function Socket(options) { // a valid `PIPE` or `TCP` descriptor this._handle = createHandle(fd, false); - err = typeof this._handle.open === 'function' ? this._handle.open(fd) : 0; + err = this._handle.open(fd); // While difficult to fabricate, in some architectures // `open` may return an error code for valid file descriptors From 6e0ce432a37b2a0f25f9b8f5d302163521bf40af Mon Sep 17 00:00:00 2001 From: Sadhbh Code Date: Wed, 17 Jun 2026 14:59:55 +0100 Subject: [PATCH 39/65] Updated wasix test plans --- plans/wasix-plan/index.md | 3 +- .../wasix-libc/05_loopback_reverse_lookup.md | 147 ++++++++++++----- ...nected_udp_send_peer_state_and_emsgsize.md | 112 ++++++------- ...iovec_boundaries_for_writev_and_sendmsg.md | 153 ++++++++++++++++++ plans/wasix-plan/wasmer/README.md | 3 +- 5 files changed, 320 insertions(+), 98 deletions(-) create mode 100644 plans/wasix-plan/wasmer/07_udp_datagram_iovec_boundaries_for_writev_and_sendmsg.md diff --git a/plans/wasix-plan/index.md b/plans/wasix-plan/index.md index 5d73e6ea4..111521e01 100644 --- a/plans/wasix-plan/index.md +++ b/plans/wasix-plan/index.md @@ -50,7 +50,8 @@ socket modes, stat modes, or process behavior. ### Wasmer - [UDP Datagram Receive, Readiness, and Backlog](wasmer/01_udp_datagram_receive_readiness_and_backlog.md) -- [Connected UDP Send, Peer State, and `EMSGSIZE`](wasmer/02_connected_udp_send_peer_state_and_emsgsize.md) +- [Connected UDP Peer State and `EMSGSIZE`](wasmer/02_connected_udp_send_peer_state_and_emsgsize.md) +- [UDP Datagram Iovec Boundaries for `writev()` and `sendmsg()`](wasmer/07_udp_datagram_iovec_boundaries_for_writev_and_sendmsg.md) - [Last Socket Error / `SO_ERROR`](wasmer/03_last_socket_error_so_error.md) - [`proc_spawn3` / `proc_exec4` for Real argv/envp](wasmer/04_proc_spawn3_proc_exec4_for_real_argv_envp.md) - [Partial Success for `fd_write`](wasmer/05_partial_success_for_fd_write.md) diff --git a/plans/wasix-plan/wasix-libc/05_loopback_reverse_lookup.md b/plans/wasix-plan/wasix-libc/05_loopback_reverse_lookup.md index 7465cec92..54bf8d381 100644 --- a/plans/wasix-plan/wasix-libc/05_loopback_reverse_lookup.md +++ b/plans/wasix-plan/wasix-libc/05_loopback_reverse_lookup.md @@ -1,15 +1,29 @@ -# Loopback Reverse Lookup +# Loopback Reverse Lookup Through /etc/hosts And Fallback Why this is a problem: In the baseline resolver path, `getnameinfo()` can fail to return `localhost` -for IPv4 or IPv6 loopback addresses. That pushes guests toward hard-coded -application-level loopback answers, which is the wrong layer. A POSIX-like libc -should provide ordinary loopback reverse lookup behavior itself, using -`/etc/hosts` where available and a small loopback fallback where appropriate. +for IPv4 or IPv6 loopback addresses when the WASIX guest has no usable +`/etc/hosts` and no useful PTR DNS answer. That pushes guests toward +application-level loopback answers, which is the wrong layer. -The fix will make `getnameinfo()` recognize loopback addresses by family and -return `localhost` when no hosts-file result has already filled the name. +The normal POSIX-shaped configuration is still to provide loopback entries +through `/etc/hosts`. The WASIX package or runner should provide an `/etc/hosts` +file and mount it into the guest as `/etc/hosts`. + +The required hosts entries are: + +```text +127.0.0.1 localhost +::1 localhost ip6-localhost ip6-loopback +``` + +However, keeping a libc/runtime fallback for the canonical loopback addresses is +also acceptable. POSIX specifies the API behavior, but leaves the underlying name +service data source as an implementation detail of the operating environment. +That data source may be `/etc/hosts`, DNS, NSS, a platform resolver, or a small +built-in answer for reserved addresses. A fallback for `127.0.0.1` and `::1` +therefore does not have to be treated as an application workaround. Minimal Example: @@ -46,60 +60,119 @@ Current problematic path: C getnameinfo(127.0.0.1, NI_NAMEREQD) -> wasix-libc libc-top-half/musl/src/network/getnameinfo.c -> reverse_hosts(...) - -> /etc/hosts may be unavailable or may not fill result - -> no loopback fallback by family - HERE IS THE PROBLEM: 127.0.0.1/::1 can remain unnamed - -> getnameinfo returns failure or numeric fallback - HERE IS THE PROBLEM: guest does not get normal localhost reverse lookup + -> opens /etc/hosts inside the guest + HERE IS THE PROBLEM: /etc/hosts may be missing or not mounted + -> DNS PTR path is unavailable or does not answer localhost + -> no reserved-address fallback exists + HERE IS THE PROBLEM: the WASIX environment has no remaining name-service + source for the canonical loopback name + -> NI_NAMEREQD returns EAI_NONAME, or without NI_NAMEREQD numeric fallback wins +``` + +Related forward lookup path: + +```text +C getaddrinfo("localhost", ...) + -> wasix-libc libc-top-half/musl/src/network/lookup_name.c + -> name_from_hosts(...) + -> opens /etc/hosts inside the guest + HERE IS THE SAME CONFIGURATION PROBLEM: without /etc/hosts, localhost + forward lookup is not configured the way a normal POSIX environment is + configured ``` Proposed solution: -In libc resolver code, switch on address family and return `localhost` for IPv4 -and IPv6 loopback. Also support a mounted `/etc/hosts`; do not create synthetic -hosts files in Wasmer. +Use both layers, with clear ownership: + +1. Provide the expected hosts file in the WASIX guest and mount it as + `/etc/hosts`. This is the normal configuration path and fixes both forward + and reverse lookup. +2. Keep a narrow `wasix-libc` reverse-lookup fallback for the reserved loopback + addresses `127.0.0.1` and `::1` when `/etc/hosts` and PTR lookup do not + produce a name. This is an implementation-defined resolver data source, not + an EdgeJS or application-level workaround. -Relevant wasix-libc code paths: +For EdgeJS QuickJS WASIX, the package should include a hosts file such as: ```text -libc-top-half/musl/src/network/getnameinfo.c - getnameinfo(...) - after reverse_hosts(...), if host buffer is empty, call reverse_loopback(...) +quickjs-wasm/etc/hosts +``` + +with: - reverse_loopback(...) - switch on AF_INET / AF_INET6 - map 127.0.0.1 and ::1 to localhost +```text +127.0.0.1 localhost +::1 localhost ip6-localhost ip6-loopback ``` -Proposed callgraph: +and the Wasmer package or runner should mount it as `/etc/hosts`, for example by +mounting the containing directory as `/etc`: + +```text +quickjs-wasm/etc:/etc +``` + +or, if the package format supports file mounts directly: + +```text +quickjs-wasm/etc/hosts:/etc/hosts +``` + +Relevant code paths: + +```text +wasix-libc + libc-top-half/musl/src/network/getnameinfo.c + reverse_hosts(...) + reads /etc/hosts for address -> name + reverse_loopback(...) + if reverse_hosts/PTR do not provide a name, map canonical loopback + addresses to localhost + +wasix-libc + libc-top-half/musl/src/network/lookup_name.c + name_from_hosts(...) + reads /etc/hosts for name -> address + +edgejs package / runner + quickjs-wasm/etc/hosts + wasmer.toml or runner volume configuration + mounts hosts file into guest as /etc/hosts +``` + +Proposed reverse lookup callgraph: ```text C getnameinfo(127.0.0.1, NI_NAMEREQD) -> wasix-libc getnameinfo(...) -> reverse_hosts(...) - -> if result is still empty, reverse_loopback(...) + -> open /etc/hosts inside guest + -> if hosts contains 127.0.0.1 localhost, caller receives localhost + -> otherwise PTR lookup is attempted where available + -> if still empty, reverse_loopback(...) -> family == AF_INET and addr == 127.0.0.1 - -> host = "localhost" -> caller receives localhost ``` -Sketch: +Forward lookup remains configuration-driven: -```c -switch (family) { -case AF_INET: - if (!memcmp(addr, "\x7f\x00\x00\x01", 4)) - strcpy(buf, "localhost"); - break; -case AF_INET6: - if (is_ipv6_loopback(addr, scopeid)) - strcpy(buf, "localhost"); - break; -} +```text +C getaddrinfo("localhost", ...) + -> wasix-libc __lookup_name(...) + -> name_from_hosts(...) + -> open /etc/hosts inside guest + -> find 127.0.0.1 / ::1 localhost entries + -> caller receives loopback addresses ``` +This keeps the preferred configuration in `/etc/hosts`, while still letting the +WASIX libc/runtime provide a small, deterministic fallback for reserved loopback +addresses when the guest filesystem does not provide one. + ## Proposed Solution References ### Commits without PR - Sadhbh: wasix-libc [1a028f0e9](https://github.com/Anodized-Titanium/wasix-libc/commit/1a028f0e9aeaffa580fc6bc5962069531fda322d) Reverse loopback +- Sadhbh: edgejs [dc9a0465](https://github.com/wasmerio/edgejs/commit/dc9a04651f74fd820f9bba7ec97ec6f342929df6) Added /etc/hosts, removed stream type normalization diff --git a/plans/wasix-plan/wasmer/02_connected_udp_send_peer_state_and_emsgsize.md b/plans/wasix-plan/wasmer/02_connected_udp_send_peer_state_and_emsgsize.md index de5dba426..31ed4cb1f 100644 --- a/plans/wasix-plan/wasmer/02_connected_udp_send_peer_state_and_emsgsize.md +++ b/plans/wasix-plan/wasmer/02_connected_udp_send_peer_state_and_emsgsize.md @@ -1,33 +1,29 @@ -# Connected UDP Send, Peer State, and `EMSGSIZE` +# Connected UDP Peer State and `EMSGSIZE` Why this is a problem: -In the baseline runtime, connected UDP loses two pieces of POSIX socket -semantics. +In the baseline runtime, connected UDP can lose POSIX socket state after +`connect()`. -First, `connect()` can auto-bind or upgrade a UDP pre-socket, but the resulting -Wasmer socket state does not reliably keep the concrete connected socket and its -peer address together. After that, `getpeername()` can report `ENOTCONN` or an -empty peer even though `connect()` succeeded, and connected sends may run through -a path that no longer knows which peer should receive the datagram. +`connect()` can auto-bind or upgrade a UDP pre-socket, but the resulting Wasmer +socket state does not reliably keep the concrete connected socket and its peer +address together. After that, `getpeername()` can report `ENOTCONN` or an empty +peer even though `connect()` succeeded, and connected sends may run through a +path that no longer knows which peer should receive the datagram. -Second, vectored writes to a connected UDP socket can be treated like stream -writes: each iovec can be sent separately. That is wrong for UDP. A single -`writev()` / `sendmsg()` call on a connected UDP socket represents one datagram -containing the concatenated iovec bytes. If the runtime sends one datagram per -iovec, the receiver observes `"he"` and `"llo"` as separate packets instead of -one `"hello"` packet. +Oversized UDP sends have a related error-mapping problem. Host networking can +report a datagram as too large, but if the virtual network layer maps that to a +generic I/O error, JavaScript and POSIX callers see the wrong error class. The +runtime should preserve `EMSGSIZE` / `Errno::Msgsize`. The fix makes Wasmer preserve the connected UDP peer after `connect()`, make -`getpeername()` read that stored peer, and make connected vectored UDP send -coalesce all iovecs into one datagram before calling the virtual network layer. -Oversized coalesced packets should fail as `EMSGSIZE` / `Errno::Msgsize`, not as -a generic I/O error. +`getpeername()` read that stored peer, route connected `send()` through that +peer, and map oversized datagrams to `EMSGSIZE` / `Errno::Msgsize` rather than a +generic I/O error. When it occurs: -- `test-dgram-connect-send-multi-string-array`; -- `test-dgram-connect-send-multi-buffer-copy`; +- `test-dgram-connect-send-*` rows that use connected UDP; - `test-dgram-msgsize`; - c-ares connected UDP resolver paths. @@ -38,7 +34,6 @@ Minimal Example: #include #include #include -#include #include int main(void) { @@ -60,15 +55,15 @@ int main(void) { getpeername(sender, (struct sockaddr *)&peer, &peer_len); printf("connected UDP peer port: %u\n", ntohs(peer.sin_port)); - struct iovec iov[2] = { - {.iov_base = "he", .iov_len = 2}, - {.iov_base = "llo", .iov_len = 3}, - }; - writev(sender, iov, 2); + send(sender, "hello", 5, 0); char buf[16] = {0}; - ssize_t n = recvfrom(receiver, buf, sizeof(buf), 0, NULL, NULL); - printf("received %zd bytes: %.*s\n", n, (int)n, buf); + struct sockaddr_in peer_from = {0}; + socklen_t peer_from_len = sizeof(peer_from); + ssize_t n = recvfrom(receiver, buf, sizeof(buf), 0, + (struct sockaddr *)&peer_from, &peer_from_len); + printf("received %zd bytes from %s:%u: %.*s\n", n, + inet_ntoa(peer_from.sin_addr), ntohs(peer_from.sin_port), (int)n, buf); close(sender); close(receiver); @@ -95,13 +90,12 @@ C getpeername(sender) -> may fall through to virtual-net addr_peer() returning None/NotConnected HERE IS THE PROBLEM: getpeername() can report ENOTCONN after connect() -C writev(sender, ["he", "llo"]) - -> wasix-libc writev()/sendmsg-shaped path - -> Wasmer connected UDP send path - -> each iovec can be treated like an independent send buffer - -> virtual-net try_send_to("he", peer) - -> virtual-net try_send_to("llo", peer) - HERE IS THE PROBLEM: receiver observes two UDP datagrams, not one +C send(sender, "hello", 5, 0) + -> wasix-libc send() + -> Wasmer lib/wasix/src/syscalls/wasix/sock_send.rs + -> Wasmer lib/wasix/src/net/socket.rs InodeSocket::send(...) + -> may not have a stored UDP peer + HERE IS THE PROBLEM: connected send can fail as NotConnected ``` Proposed solution: @@ -111,8 +105,7 @@ Proposed solution: - Store the connected peer address in Wasmer socket state. - Implement `addr_peer()` / `getpeername()` for connected UDP by returning the stored peer. -- For connected UDP vectored send, concatenate iovecs into one temporary packet - and send exactly one datagram to the stored peer. +- Route connected UDP `send()` to the stored peer. - Map oversized datagrams to `Errno::Msgsize`, not a generic I/O error. Relevant Wasmer code paths: @@ -125,8 +118,8 @@ lib/wasix/src/net/socket.rs InodeSocket::addr_peer(...) for InodeSocketKind::UdpSocket, return the stored peer first - connected UDP send path / sock_send_msg path - coalesce iovecs before calling virtual-net + InodeSocket::send(...) + for connected InodeSocketKind::UdpSocket, send to stored peer map NetworkError::MessageSize to Errno::Msgsize lib/virtual-net/src/host.rs @@ -152,30 +145,31 @@ C getpeername(sender) -> Wasmer InodeSocket::addr_peer() -> return socket peer = Some(peer) -C writev(sender, ["he", "llo"]) - -> wasix-libc writev()/sendmsg-shaped path - -> Wasmer connected UDP send path - -> coalesce iovecs into "hello" +C send(sender, "hello", 5, 0) + -> wasix-libc send() + -> Wasmer sock_send_internal() + -> Wasmer InodeSocket::send(...) -> virtual-net try_send_to("hello", peer) - -> receiver gets exactly one datagram + -> receiver gets one datagram from the connected sender ``` Sketch: ```rust -fn udp_connected_send(iovs: &[IoSlice<'_>], peer: SocketAddr) -> Result { - let len: usize = iovs.iter().map(|iov| iov.len()).sum(); - if len > max_udp_payload() { - return Err(Errno::Msgsize); - } - - let mut packet = Vec::with_capacity(len); - for iov in iovs { - packet.extend_from_slice(iov); - } - - socket.send_to(&packet, peer).map_err(map_socket_err)?; - Ok(len) +fn udp_connect(socket: InodeSocket, peer: SocketAddr) -> Result, Errno> { + let bound_socket = socket.auto_bind_udp(tasks, net).await?; + let socket = bound_socket.clone().unwrap_or(socket); + let connected_socket = socket.connect_udp_in_place(peer)?; + Ok(connected_socket.or(bound_socket)) +} + +fn udp_connected_send(buf: &[u8], peer: SocketAddr) -> Result { + socket + .send_to(buf, peer) + .map_err(|err| match err { + NetworkError::MessageSize => Errno::Msgsize, + other => map_socket_err(other), + }) } ``` @@ -183,5 +177,5 @@ fn udp_connected_send(iovs: &[IoSlice<'_>], peer: SocketAddr) -> Result +#include +#include +#include +#include +#include + +int main(void) { + int receiver = socket(AF_INET, SOCK_DGRAM, 0); + int sender = socket(AF_INET, SOCK_DGRAM, 0); + + struct sockaddr_in addr = {0}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr.sin_port = 0; + bind(receiver, (struct sockaddr *)&addr, sizeof(addr)); + + socklen_t len = sizeof(addr); + getsockname(receiver, (struct sockaddr *)&addr, &len); + connect(sender, (struct sockaddr *)&addr, sizeof(addr)); + + struct iovec iov[2] = { + {.iov_base = "he", .iov_len = 2}, + {.iov_base = "llo", .iov_len = 3}, + }; + writev(sender, iov, 2); + + char buf[16] = {0}; + ssize_t n = recvfrom(receiver, buf, sizeof(buf), 0, NULL, NULL); + printf("received %zd bytes: %.*s\n", n, (int)n, buf); + + close(sender); + close(receiver); +} +``` + +Callgraph and boundary: + +Current problematic `writev()` path: + +```text +C writev(sender, ["he", "llo"]) + -> wasix-libc writev() + -> WASI fd_write + -> Wasmer lib/wasix/src/syscalls/wasi/fd_write.rs fd_write_internal(...) + -> socket fd branch iterates iovecs + -> InodeSocket::send("he") + -> InodeSocket::send("llo") + HERE IS THE PROBLEM: two UDP datagrams are emitted instead of one +``` + +Future `sendmsg()` path has the same datagram-boundary requirement: + +```text +C sendmsg(sender, msg_iov=["he", "llo"], control=...) + -> wasix-libc sendmsg() + -> WASIX sendmsg-capable syscall boundary + -> Wasmer sendmsg implementation + -> payload iovecs must be one datagram + -> control data / SCM_RIGHTS handling is separate future work +``` + +Proposed solution: + +- Detect datagram sockets on vectored send paths. +- For datagram sockets, copy all iovec bytes into one temporary packet and send + exactly one datagram. +- Keep stream sockets on the existing per-iovec/partial-progress behavior. +- Keep `sendmsg()` control-data and fd-passing work in the future `sendmsg()` / + `recvmsg()` plan; this page is only about payload datagram boundaries. + +Relevant Wasmer code paths: + +```text +lib/wasix/src/syscalls/wasi/fd_write.rs + fd_write_internal(...) + socket fd branch for POSIX writev() + +lib/wasix/src/syscalls/wasix/sock_send.rs + sock_send_internal(...) + existing WASIX send path with datagram iovec coalescing behavior + +lib/wasix/src/syscalls/wasix/sock_send_to.rs + sock_send_to_internal(...) + send-to datagram path should preserve one syscall as one datagram +``` + +Proposed callgraph: + +```text +C writev(sender, ["he", "llo"]) + -> wasix-libc writev() + -> WASI fd_write + -> Wasmer fd_write_internal(...) + -> detect datagram socket + -> coalesce iovecs into "hello" + -> InodeSocket::send("hello") once + -> receiver gets exactly one datagram +``` + +Sketch: + +```rust +fn datagram_iov_send(iovs: &[IoSlice<'_>], peer: SocketAddr) -> Result { + let len: usize = iovs.iter().map(|iov| iov.len()).sum(); + let mut packet = Vec::with_capacity(len); + for iov in iovs { + packet.extend_from_slice(iov); + } + + socket.send_to(&packet, peer).map_err(map_socket_err)?; + Ok(len) +} +``` + +## Proposed Solution References + +### [wasmerio/wasmer#6685: Udp datagram receive and last err](https://github.com/wasmerio/wasmer/pull/6685) + +- Sadhbh: wasmer [50e5c1f1375](https://github.com/Anodized-Titanium/wasmer/commit/50e5c1f137523e683e1b52a8f847dcdc35ca0b37) Connected UDP send iovec coalescing. + +### Related Plan + +- [Future: `sendmsg` / `recvmsg` Control Data and FD Passing](06_future_sendmsg_recvmsg_control_data_and_fd_passing.md) diff --git a/plans/wasix-plan/wasmer/README.md b/plans/wasix-plan/wasmer/README.md index 82eff47e1..1e3bbfe6d 100644 --- a/plans/wasix-plan/wasmer/README.md +++ b/plans/wasix-plan/wasmer/README.md @@ -1,7 +1,8 @@ # Wasmer Plan - [UDP Datagram Receive, Readiness, and Backlog](01_udp_datagram_receive_readiness_and_backlog.md) -- [Connected UDP Send, Peer State, and `EMSGSIZE`](02_connected_udp_send_peer_state_and_emsgsize.md) +- [Connected UDP Peer State and `EMSGSIZE`](02_connected_udp_send_peer_state_and_emsgsize.md) +- [UDP Datagram Iovec Boundaries for `writev()` and `sendmsg()`](07_udp_datagram_iovec_boundaries_for_writev_and_sendmsg.md) - [Last Socket Error / `SO_ERROR`](03_last_socket_error_so_error.md) - [`proc_spawn3` / `proc_exec4` for Real argv/envp](04_proc_spawn3_proc_exec4_for_real_argv_envp.md) - [Partial Success for `fd_write`](05_partial_success_for_fd_write.md) From 172b1843ff7e5cebced88f9c4d5bd2fe70ef581e Mon Sep 17 00:00:00 2001 From: Sadhbh Code Date: Mon, 22 Jun 2026 21:21:46 +0100 Subject: [PATCH 40/65] Updated sysroot and wasmer.toml --- .github/workflows/test-and-build-quickjs.yml | 2 +- .github/workflows/test-and-build.yml | 2 +- quickjs-wasm/etc/hosts | 2 +- quickjs-wasm/wasmer.toml | 3 +++ 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-and-build-quickjs.yml b/.github/workflows/test-and-build-quickjs.yml index 3439ee1fb..33ab19abc 100644 --- a/.github/workflows/test-and-build-quickjs.yml +++ b/.github/workflows/test-and-build-quickjs.yml @@ -179,7 +179,7 @@ jobs: uses: wasix-org/wasixcc@v0.4.3 with: github_token: ${{ secrets.GITHUB_TOKEN }} - sysroot-tag: v2026-05-26.1 + sysroot-tag: v2026-06-18.1 version: v0.4.3 - name: Build edge (QuickJS WASIX) diff --git a/.github/workflows/test-and-build.yml b/.github/workflows/test-and-build.yml index 74ad7fd39..715668e08 100644 --- a/.github/workflows/test-and-build.yml +++ b/.github/workflows/test-and-build.yml @@ -189,7 +189,7 @@ jobs: uses: wasix-org/wasixcc@v0.4.3 with: github_token: ${{ secrets.GITHUB_TOKEN }} - sysroot-tag: v2026-05-26.1 + sysroot-tag: v2026-06-18.1 version: v0.4.3 - name: Install wasm-tools diff --git a/quickjs-wasm/etc/hosts b/quickjs-wasm/etc/hosts index 77c24a17a..7db0d689e 100644 --- a/quickjs-wasm/etc/hosts +++ b/quickjs-wasm/etc/hosts @@ -1,2 +1,2 @@ 127.0.0.1 localhost -::1 localhost +::1 localhost ip6-localhost ip6-loopback \ No newline at end of file diff --git a/quickjs-wasm/wasmer.toml b/quickjs-wasm/wasmer.toml index 6e308fd6e..e2e1b0323 100644 --- a/quickjs-wasm/wasmer.toml +++ b/quickjs-wasm/wasmer.toml @@ -16,3 +16,6 @@ module = "edge" [fs] "/etc" = "./etc" "/usr/local/ssl" = "../ssl-certs" + +[dependencies] +"wasmer/bash" = "1.0.25" \ No newline at end of file From 21560293efd54609b2f5356f16b1e869bd97f412 Mon Sep 17 00:00:00 2001 From: Arshia Ghafoori Date: Tue, 23 Jun 2026 16:43:11 +0000 Subject: [PATCH 41/65] Use FD 10 for IPC socket to avoid the WASIX preopen FDs --- lib/internal/child_process.js | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/lib/internal/child_process.js b/lib/internal/child_process.js index dada6b8cc..577a3dff9 100644 --- a/lib/internal/child_process.js +++ b/lib/internal/child_process.js @@ -981,6 +981,13 @@ function isInternal(message) { const nop = FunctionPrototype; +// WASIX (Wasmer) preopens directories on FDs 3 through 5 (virtual root, +// "/", and "."). proc_spawn refuses dup2 onto preopened FDs, which breaks +// Node's default IPC stdio slot at index 3 with ENOTSUP. Use FD 10 to stay +// clear of the reserved range with extra headroom. +const kWasixIpcFd = 10; +const isWasix = process.platform === 'wasi'; + function getValidStdio(stdio, sync) { let ipc; let ipcFd; @@ -1079,6 +1086,29 @@ function getValidStdio(stdio, sync) { return acc; }, []); + if (ipc !== undefined && isWasix) { + let ipcEntryIndex = -1; + for (let n = 0; n < stdio.length; n++) { + if (stdio[n].ipc) { + ipcEntryIndex = n; + break; + } + } + if (ipcEntryIndex !== -1 && ipcEntryIndex !== kWasixIpcFd) { + const ipcEntry = stdio[ipcEntryIndex]; + stdio[ipcEntryIndex] = { type: 'ignore' }; + while (stdio.length < kWasixIpcFd) { + ArrayPrototypePush(stdio, { type: 'ignore' }); + } + if (stdio.length === kWasixIpcFd) { + ArrayPrototypePush(stdio, ipcEntry); + } else { + stdio[kWasixIpcFd] = ipcEntry; + } + } + ipcFd = kWasixIpcFd; + } + return { stdio, ipc, ipcFd }; } From 77bd2e18ead90b0d10a4f066a95532b8ab809a52 Mon Sep 17 00:00:00 2001 From: Arshia Ghafoori Date: Tue, 23 Jun 2026 19:29:35 +0000 Subject: [PATCH 42/65] Bump libuv-wasix to wasix-1.51.0 with merged posix_spawn PR #7. Track cb7e09ae on wasix-1.51.0 so WASIX child_process uses the squashed spawn, waitpid polling, and EH-gated fork fallback from wasix-org/libuv#7. Co-authored-by: Cursor --- deps/libuv-wasix | 2 +- .../01_use_posix_spawn_on_wasix.md | 19 +++++++++++++------ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/deps/libuv-wasix b/deps/libuv-wasix index 0ae770b9d..cb7e09aed 160000 --- a/deps/libuv-wasix +++ b/deps/libuv-wasix @@ -1 +1 @@ -Subproject commit 0ae770b9daae35d97d3c9a2693a5b22362124f12 +Subproject commit cb7e09aed2fb784255d108d7c78c2063a61b3865 diff --git a/plans/wasix-plan/libuv-wasix/01_use_posix_spawn_on_wasix.md b/plans/wasix-plan/libuv-wasix/01_use_posix_spawn_on_wasix.md index 4eccac0ea..3a4ac6dd1 100644 --- a/plans/wasix-plan/libuv-wasix/01_use_posix_spawn_on_wasix.md +++ b/plans/wasix-plan/libuv-wasix/01_use_posix_spawn_on_wasix.md @@ -12,10 +12,17 @@ This occurs whenever Node uses `child_process.spawn()`, `execFile()`, `exec()`, Branch context: -`libuv-wasix` uses `ubi` as its mainline branch. In the local history, `ubi` -points at Martin's `ea792e22` "disable fork" baseline. The spawn proposal is -the next commit on `fix/spawn`: Artem's `ba06698b`, which replaces the disabled -fork path with WASIX `posix_spawn` / `proc_join` behavior. +Merged to [`wasix-1.51.0`](https://github.com/wasix-org/libuv/tree/wasix-1.51.0) as +[squashed PR #7](https://github.com/wasix-org/libuv/pull/7) ([`cb7e09ae`](https://github.com/wasix-org/libuv/commit/cb7e09aed2fb784255d108d7c78c2063a61b3865)). +The branch includes Martin's ubi WASIX guards (`876682c4`, `767df34a`), UDP shims, +and the posix_spawn rework without the `ea792e22` fake fork stub. + +Compared to Artem's original `ba06698b`, the merged PR intentionally: + +- uses libc `waitpid(WNOHANG)` instead of direct `__wasi_proc_join()` in libuv +- returns `UV_ENOSYS` from fork only when `__wasm_exception_handling__` is enabled +- omits `uv_exepath()` argv0 saving in `no-proctitle.c` (EdgeJS sets `process.execPath`) +- keeps `uv_pipe()` unidirectional stdio setup needed for stdout capture on WASIX Minimal Example: @@ -89,5 +96,5 @@ stdio container into `posix_spawn_file_actions_*` calls plus `posix_spawnp()`. ## Related Commits -- Martin: libuv-wasix [ea792e22](https://github.com/wasix-org/libuv/commit/ea792e22c8e88149d8279883ab4c7fd1fd716392) disable fork -- Artem: libuv-wasix [ba06698b](https://github.com/Anodized-Titanium/libuv-wasix/commit/ba06698ba3f3b508a7a4eeacb23e0c911ebf4576) libuv-wasix: use WASIX posix_spawn/proc_join for child processes +- EdgeJS submodule: [`cb7e09ae`](https://github.com/wasix-org/libuv/commit/cb7e09aed2fb784255d108d7c78c2063a61b3865) on `wasix-1.51.0` ([PR #7](https://github.com/wasix-org/libuv/pull/7)) +- Prior art: Artem [ba06698b](https://github.com/Anodized-Titanium/libuv-wasix/commit/ba06698ba3f3b508a7a4eeacb23e0c911ebf4576) (superseded by the POSIX-boundary cleanup above) From 351df744fc6654edebbc1ca2b3a1eb4674a234a4 Mon Sep 17 00:00:00 2001 From: Arshia Ghafoori Date: Wed, 24 Jun 2026 07:50:16 +0000 Subject: [PATCH 43/65] Remove the upstream node submodule and its build wiring. EdgeJS already vendors Node lib/, src/, and deps/ at the repo root, so drop the redundant nodejs/node checkout along with CI checks, CMake NAPI_V8_NODE_ROOT plumbing, and WASIX workspace mounting for ./node. Co-authored-by: Cursor --- .github/workflows/test-and-build-quickjs.yml | 2 -- .github/workflows/test-and-build.yml | 2 -- .gitmodules | 4 ---- cmake/EdgeOptions.cmake | 9 --------- napi | 2 +- node | 1 - scripts/edge-wasix-node-runner.sh | 2 +- tests/CMakeLists.txt | 1 - tests/runners/test_1_cli_phase01.cc | 10 ++-------- tests/runners/test_3_node_dropin_subset_phase02.cc | 14 ++++---------- 10 files changed, 8 insertions(+), 39 deletions(-) delete mode 160000 node diff --git a/.github/workflows/test-and-build-quickjs.yml b/.github/workflows/test-and-build-quickjs.yml index 33ab19abc..a8ab0c25d 100644 --- a/.github/workflows/test-and-build-quickjs.yml +++ b/.github/workflows/test-and-build-quickjs.yml @@ -62,7 +62,6 @@ jobs: set -euo pipefail git submodule sync --recursive git -c protocol.version=2 submodule update --init --recursive --depth=1 - test -f node/src/node_api_types.h - name: Install Linux build dependencies shell: bash @@ -128,7 +127,6 @@ jobs: set -euo pipefail git submodule sync --recursive git -c protocol.version=2 submodule update --init --recursive --depth=1 - test -f node/src/node_api_types.h - name: Validate public N-API headers stay V8-agnostic shell: bash diff --git a/.github/workflows/test-and-build.yml b/.github/workflows/test-and-build.yml index 715668e08..dde6877a8 100644 --- a/.github/workflows/test-and-build.yml +++ b/.github/workflows/test-and-build.yml @@ -63,7 +63,6 @@ jobs: set -euo pipefail git submodule sync --recursive git -c protocol.version=2 submodule update --init --recursive --depth=1 - test -f node/src/node_api_types.h - name: Install Linux build dependencies shell: bash @@ -130,7 +129,6 @@ jobs: set -euo pipefail git submodule sync --recursive git -c protocol.version=2 submodule update --init --recursive --depth=1 - test -f node/src/node_api_types.h - name: Validate public N-API headers stay V8-agnostic shell: bash diff --git a/.gitmodules b/.gitmodules index 75c7e4796..775c97e6c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,7 +1,3 @@ -[submodule "node"] - path = node - url = https://github.com/nodejs/node.git - branch = v24.x [submodule "test"] path = test url = https://github.com/wasmerio/node-test.git diff --git a/cmake/EdgeOptions.cmake b/cmake/EdgeOptions.cmake index 5684e42d8..fd5a1e9eb 100644 --- a/cmake/EdgeOptions.cmake +++ b/cmake/EdgeOptions.cmake @@ -4,14 +4,6 @@ function(edge_configure_options) CACHE STRING "Default Wasmer package used by edge --safe" ) - set(NAPI_V8_NODE_ROOT_DEFAULT "${PROJECT_ROOT}/node") - if(DEFINED ENV{NAPI_V8_NODE_ROOT}) - set(NAPI_V8_NODE_ROOT_DEFAULT "$ENV{NAPI_V8_NODE_ROOT}") - endif() - set(NAPI_V8_NODE_ROOT - "${NAPI_V8_NODE_ROOT_DEFAULT}" - CACHE PATH "Path to Node source root (for raw Node tests/fixtures)" - ) option(EDGE_EXTERNAL_NAPI_V8 "Deprecated alias. Use EDGE_NAPI_PROVIDER=imports." OFF @@ -61,7 +53,6 @@ function(edge_configure_options) endif() set(EDGE_DEFAULT_WASMER_PACKAGE "${EDGE_DEFAULT_WASMER_PACKAGE}" PARENT_SCOPE) - set(NAPI_V8_NODE_ROOT "${NAPI_V8_NODE_ROOT}" PARENT_SCOPE) set(EDGE_EXTERNAL_NAPI_V8 "${EDGE_EXTERNAL_NAPI_V8}" PARENT_SCOPE) set(EDGE_ALLOW_UNDEFINED_IMPORTS "${EDGE_ALLOW_UNDEFINED_IMPORTS}" PARENT_SCOPE) set(EDGE_PREFER_REPO_LOCAL_V8 "${EDGE_PREFER_REPO_LOCAL_V8}" PARENT_SCOPE) diff --git a/napi b/napi index 5fb8bb599..3d9b93151 160000 --- a/napi +++ b/napi @@ -1 +1 @@ -Subproject commit 5fb8bb59976da6a8ef8544d25b23ff796ec70744 +Subproject commit 3d9b93151cfd5c7a90ed299251fbf69f760f675d diff --git a/node b/node deleted file mode 160000 index 71fb7c696..000000000 --- a/node +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 71fb7c69675ebda88387ada0a77a0c697d257623 diff --git a/scripts/edge-wasix-node-runner.sh b/scripts/edge-wasix-node-runner.sh index 359609f1d..cd8ea8c63 100755 --- a/scripts/edge-wasix-node-runner.sh +++ b/scripts/edge-wasix-node-runner.sh @@ -9,7 +9,7 @@ wasmer_bin="${WASMER_BIN:-wasmer}" package_dir="${WASIX_EDGEJS_PACKAGE_DIR:-${edgejs_root}/quickjs-wasm}" guest_root="${WASIX_EDGEJS_GUEST_ROOT:-/workspace}" guest_test_tmp_root="${WASIX_EDGEJS_GUEST_TEST_TMP_ROOT:-/tmp/edgejs-node-test}" -workspace_dirs_csv="${WASIX_EDGEJS_WORKSPACE_DIRS:-test,lib,deps,node,assets,build-quickjs-wasix}" +workspace_dirs_csv="${WASIX_EDGEJS_WORKSPACE_DIRS:-test,lib,deps,assets,build-quickjs-wasix}" guest_exec_path="${WASIX_EDGEJS_GUEST_EXEC_PATH:-${guest_root}/build-quickjs-wasix/edgejs.wasm}" wasmer_stack_args=() created_run_root=0 diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 25ab2c41d..6b29d0688 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -42,7 +42,6 @@ if(TARGET napi_v8 AND NAPI_V8_LIBRARY) ) target_compile_definitions(${target_name} PRIVATE - NAPI_V8_NODE_ROOT_PATH="${NAPI_V8_NODE_ROOT}" PROJECT_ROOT_PATH="${PROJECT_ROOT}" NAPI_V8_ROOT_PATH="${EDGE_ROOT}" EDGE_VERSION_COMMIT="${EDGE_VERSION_COMMIT}" diff --git a/tests/runners/test_1_cli_phase01.cc b/tests/runners/test_1_cli_phase01.cc index bdd7949b2..4f4d81282 100644 --- a/tests/runners/test_1_cli_phase01.cc +++ b/tests/runners/test_1_cli_phase01.cc @@ -70,16 +70,10 @@ std::filesystem::path ResolveBuiltEdgeenvBinary() { return ResolveBuiltBinary("edgeenv"); } -#if defined(NAPI_V8_NODE_ROOT_PATH) || defined(PROJECT_ROOT_PATH) +#if defined(PROJECT_ROOT_PATH) std::filesystem::path ResolveNodeTestScriptPath(const char* relative_path) { namespace fs = std::filesystem; -#if defined(PROJECT_ROOT_PATH) fs::path test_root(PROJECT_ROOT_PATH "/test"); -#elif defined(NAPI_V8_NODE_ROOT_PATH) - fs::path test_root = fs::path(NAPI_V8_NODE_ROOT_PATH).parent_path() / "test"; -#else - fs::path test_root("test"); -#endif if (!test_root.is_absolute()) { test_root = fs::absolute(test_root).lexically_normal(); } @@ -1330,7 +1324,7 @@ TEST_F(Test1CliPhase01, SerdesBindingMatchesNodeContractAndHostObjectSemantics) TEST_F(Test1CliPhase01, SerdesPassesRawNodeV8SerdesScript) { #if defined(_WIN32) GTEST_SKIP() << "Serdes raw Node subprocess parity check is POSIX-only"; -#elif !defined(NAPI_V8_NODE_ROOT_PATH) && !defined(PROJECT_ROOT_PATH) +#elif !defined(PROJECT_ROOT_PATH) GTEST_SKIP() << "Node test root path is unavailable"; #else const auto edge_path = ResolveBuiltEdgeBinary(); diff --git a/tests/runners/test_3_node_dropin_subset_phase02.cc b/tests/runners/test_3_node_dropin_subset_phase02.cc index 8840f0f09..d06e5fa66 100644 --- a/tests/runners/test_3_node_dropin_subset_phase02.cc +++ b/tests/runners/test_3_node_dropin_subset_phase02.cc @@ -234,16 +234,10 @@ bool ScriptHasUnsupportedFlagsHeader(const std::filesystem::path& script_path) { return false; } -#if defined(NAPI_V8_NODE_ROOT_PATH) || defined(PROJECT_ROOT_PATH) +#if defined(PROJECT_ROOT_PATH) std::filesystem::path ResolveNodeTestRootPathForRawScript(std::string_view script_rel) { namespace fs = std::filesystem; -#if defined(PROJECT_ROOT_PATH) fs::path node_test_root_path(PROJECT_ROOT_PATH "/test"); -#elif defined(NAPI_V8_NODE_ROOT_PATH) - fs::path node_test_root_path = fs::path(NAPI_V8_NODE_ROOT_PATH).parent_path() / "test"; -#else - fs::path node_test_root_path("test"); -#endif if (!node_test_root_path.is_absolute()) { // Resolve relative test_root by walking up from cwd until we find // test//