From 56fbbc2f08e6cf31d51da2ae3fe7e67d86b5c6b7 Mon Sep 17 00:00:00 2001 From: fogtape Date: Wed, 14 Oct 2026 21:17:51 +0800 Subject: [PATCH 01/16] upgrade: rebase the patch series onto the official v26.11.1 tag 26.11.1 is the patch release that followed the reviewed 26.11.0 proposal snapshot. Pin the official tag (364593d0631def13e3043dd84f77ebca2c767ce5), bump the mobile version to 26.11.1-0, and regenerate the series: 21 of 22 patches are byte-identical, and only the upstream-workflow removal patch changes, absorbing the modify/delete conflict on .github/workflows/build-tarball.yml (resolution: deletion, per the patch's intent). A fresh shallow reconstruction reproduces tree 2a6d2dfd8e605b6eb1a0a2c2c24c48fe92836295 (expected-tree.txt). Assisted-by: Codex --- README.md | 13 +++++++------ docs/UPSTREAM-26.11.md | 15 +++++++++++++-- docs/VERSION-LINES.md | 13 +++++++------ expected-tree.txt | 2 +- mobile-src/src/node_mobile_version.h | 2 +- ...emove-upstream-only-workflows-and-config.patch | 13 ++++--------- upstream-base.txt | 8 ++++---- 7 files changed, 37 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index f33e5725c4..5480b4dac9 100644 --- a/README.md +++ b/README.md @@ -9,16 +9,17 @@ in-app, with npm modules and N-API native addons. | Android | `libnode.so` + headers | `arm64-v8a`, `armeabi-v7a`, `x86_64` (16 KB page size) | | iOS | `NodeMobile.xcframework` | `arm64` device, `arm64` simulator | -Current line: **Node.js 26** (`v26.11.0`, upstream PR preview). -This checkout pins [official release proposal #66546](https://github.com/nodejs/node/pull/66546) -at `49072a0be4c7410982557aebf5b5c924fe7db597`, before its merge/tag. -Builds are CI previews; [provenance and validation](docs/UPSTREAM-26.11.md) -record how to move to the official tag after release. +Current line: **Node.js 26** (`v26.11.1`, official release). +This checkout pins the official tag `v26.11.1` at commit +`364593d0631def13e3043dd84f77ebca2c767ce5`; it rebases the reviewed +[proposal #66546](https://github.com/nodejs/node/pull/66546) snapshot to +the patch release upstream published afterwards. [Provenance and +validation](docs/UPSTREAM-26.11.md) record the rebase. The stable Node 24 line continues independently on [`recipe`](https://github.com/fogtape/nodejs-mobile/tree/recipe). See [maintained version lines](docs/VERSION-LINES.md) for branch selection, build instructions and download locations. `process.version` reports the upstream version unchanged, so version-parsing tools keep working; the -mobile build is identified by `process.versions.mobile` (e.g. `"26.11.0-0"`). +mobile build is identified by `process.versions.mobile` (e.g. `"26.11.1-0"`). ## Get it diff --git a/docs/UPSTREAM-26.11.md b/docs/UPSTREAM-26.11.md index c288573de1..19405e2119 100644 --- a/docs/UPSTREAM-26.11.md +++ b/docs/UPSTREAM-26.11.md @@ -1,4 +1,13 @@ -# Node 26.11 upstream PR preview +# Node 26.11 upstream line + +`upstream-base.txt` pins the official tag `v26.11.1` +(`364593d0631def13e3043dd84f77ebca2c767ce5`), the patch release that +followed the reviewed 26.11.0 proposal snapshot described below. The rebase +re-emitted 21 of the 22 patches byte-identically; the workflow removal +patch absorbed upstream's edits to `.github/workflows/build-tarball.yml` +(`modify/delete`, resolved as deletion per that patch's intent). The mobile +version is `26.11.1-0`, and `expected-tree.txt` records the new +source-tree checksum. This recipe previews official Node.js release proposal [#66546](https://github.com/nodejs/node/pull/66546), titled @@ -167,7 +176,9 @@ proposal, and the source tag records the SHA. See [RELEASING.md](RELEASING.md). ## Move to the official release -After upstream publishes `v26.11.0`, compare its commit against this pin, +This branch performed the move: it pins `v26.11.1`, the patch release that +followed the snapshot. The procedure, retained for the record: after +upstream publishes a release, compare its commit against this pin, review any additional changes, update `upstream-base.txt` to the official tag, regenerate the recipe and checksum, and rerun the platform/device gates. Default Cut release and publication still reject SHA baselines. After the diff --git a/docs/VERSION-LINES.md b/docs/VERSION-LINES.md index 57a26d4f42..a55cab9024 100644 --- a/docs/VERSION-LINES.md +++ b/docs/VERSION-LINES.md @@ -7,12 +7,13 @@ versioned together on that branch. | Branch | Upstream base | Purpose | Outputs | | --- | --- | --- | --- | | `recipe` | Node.js 24.21.0 | Stable Node 24 maintenance and releases | Android and iOS, full and lite | -| `recipe-v26` | Node.js 26.11.0 proposal at a pinned SHA | Independent Node 26 maintenance and explicit upstream snapshot prereleases | Android and iOS, full and lite | +| `recipe-v26` | Node.js 26.11.1 (official tag) | Independent Node 26 maintenance and releases | Android and iOS, full and lite | -For Node 26.11, clone `recipe-v26`; the reviewed upgrade is merged and pins -an immutable commit. Snapshot publication requires an explicit full-SHA -input in both manual workflows and its reviewed release marker; default -releases require an official upstream tag. See [26.11 provenance](UPSTREAM-26.11.md). +For Node 26.11, clone `recipe-v26`; the line now pins the official +`v26.11.1` tag. The earlier 26.11.0 proposal snapshot — and the explicit +full-SHA input its publication required — stays documented in +[26.11 provenance](UPSTREAM-26.11.md). Default releases require an +official upstream tag and a reviewed release marker. The authoritative base is always `upstream-base.txt` on the selected branch. Updating Node 26 does not change the Node 24 recipe or replace its published @@ -94,7 +95,7 @@ run URL with the downloaded files; do not mix platform slices or flavors from different runs. Versioned preview packages additionally contain `BUILD-INFO.json` and are accompanied by `SHA256SUMS`. -This preview reports `process.version` as `v26.11.0`; `process.versions.mobile` identifies +The line reports `process.version` as `v26.11.1`; `process.versions.mobile` identifies the mobile build revision. A CI preview is not a published release. **Cut release** also supports `recipe-v26`; its reviewed PR arms a specific version; after merge, manually run **Build** with `operation=prerelease` and that exact mobile version. Push/PR builds do diff --git a/expected-tree.txt b/expected-tree.txt index 1547f16ce6..1fb6ba6123 100644 --- a/expected-tree.txt +++ b/expected-tree.txt @@ -14,4 +14,4 @@ # run prepare.sh (or regenerate-patches.py, which prints the hash) and # update the value below in the same commit. If you forget, CI fails and # prints the hash it got. -366de32e0018c646ea07608448b694716229b35b +2a6d2dfd8e605b6eb1a0a2c2c24c48fe92836295 diff --git a/mobile-src/src/node_mobile_version.h b/mobile-src/src/node_mobile_version.h index 4dd80bfb8f..b45ef547bb 100644 --- a/mobile-src/src/node_mobile_version.h +++ b/mobile-src/src/node_mobile_version.h @@ -5,7 +5,7 @@ #define NODE_MOBILE_MAJOR_VERSION 26 #define NODE_MOBILE_MINOR_VERSION 11 -#define NODE_MOBILE_PATCH_VERSION 0 +#define NODE_MOBILE_PATCH_VERSION 1 // Mobile rebuild revision of the same upstream Node version: the -R in the // vX.Y.Z-R release tag. Computed and bumped by the Cut release workflow diff --git a/patches/ci-remove-upstream-only-workflows-and-config.patch b/patches/ci-remove-upstream-only-workflows-and-config.patch index b30c929ea4..2195e89048 100644 --- a/patches/ci-remove-upstream-only-workflows-and-config.patch +++ b/patches/ci-remove-upstream-only-workflows-and-config.patch @@ -27,7 +27,7 @@ is read only from the default branch and is left untouched. .github/workflows/auto-start-ci.yml | 93 ----- .github/workflows/benchmark.yml | 271 ------------- .github/workflows/build-shared.yml | 93 ----- - .github/workflows/build-tarball.yml | 137 ------- + .github/workflows/build-tarball.yml | 132 ------- .github/workflows/close-stalled.yml | 36 -- .github/workflows/codeql.yml | 44 --- .github/workflows/comment-labeled.yml | 57 --- @@ -69,7 +69,7 @@ is read only from the default branch and is left untouched. .github/workflows/update-openssl.yml | 69 ---- .github/workflows/update-v8.yml | 57 --- .github/workflows/update-wpt.yml | 95 ----- - 49 files changed, 4758 deletions(-) + 49 files changed, 4753 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/config.yml delete mode 100644 .github/SUPPORT.md delete mode 100644 .github/label-pr-config.yml @@ -946,10 +946,10 @@ index 73f7ec6afb..0000000000 - ' "$TAR_DIR/tools/nix/ci-shell.nix" diff --git a/.github/workflows/build-tarball.yml b/.github/workflows/build-tarball.yml deleted file mode 100644 -index 53e625ef88..0000000000 +index bfc8f92dd0..0000000000 --- a/.github/workflows/build-tarball.yml +++ /dev/null -@@ -1,137 +0,0 @@ +@@ -1,132 +0,0 @@ -name: Build from tarball - -on: @@ -1019,7 +1019,6 @@ index 53e625ef88..0000000000 - PYTHON_VERSION: '3.14' - FLAKY_TESTS: keep_retrying - CLANG_VERSION: '19' -- NODE_VERSION: lts/* - -permissions: - contents: read @@ -1037,10 +1036,6 @@ index 53e625ef88..0000000000 - with: - python-version: ${{ env.PYTHON_VERSION }} - allow-prereleases: true -- - name: Use Node.js ${{ env.NODE_VERSION }} -- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 -- with: -- node-version: ${{ env.NODE_VERSION }} - - name: Make tarball - run: | - export DISTTYPE=nightly diff --git a/upstream-base.txt b/upstream-base.txt index 0bb94b677e..4bace2bf7b 100644 --- a/upstream-base.txt +++ b/upstream-base.txt @@ -1,4 +1,4 @@ -# Node.js 26.11.0 release proposal, not yet an official release tag. -# https://github.com/nodejs/node/pull/66546 (v26.11.0-proposal) -# Pin the reviewed PR commit; do not follow the moving proposal branch. -49072a0be4c7410982557aebf5b5c924fe7db597 +# The upstream nodejs/node release the patch series applies to. +# First non-comment line is the tag (or SHA); update it as part of every +# upstream bump (see README.md). +v26.11.1 From 5827d3000bbe4210a268fbe6fe0da70d6b7bf3c1 Mon Sep 17 00:00:00 2001 From: fogtape Date: Wed, 14 Oct 2026 21:22:46 +0800 Subject: [PATCH 02/16] android: link a prebuilt OpenSSL 4.0.3 tree Upstream Node bundles the OpenSSL 3.5.x line; OpenSSL 4.0 is the first release with ECH (RFC 9849). Build the pinned 4.0.3 release asset with the NDK (static, no-asm, -fPIC) and link it into libnode.so through --shared-openssl behind NODEJS_MOBILE_ANDROID_OPENSSL_DIR; the bundled path and iOS are unchanged. node:quic stays disabled and no ECH API is exposed yet. - tools/android_openssl.sh builds the SHA-256-verified tarball per ABI. - android_build.sh wires it in and greps the artifact for the version. - The Build workflow re-checks every restored artifact and folds the builder script into the libnode cache key. - docs/OPENSSL-4.md records scope, verification and maintenance. A fresh shallow reconstruction reproduces tree 70b5331e853ee5c94b9501072dda8758236b9703 (expected-tree.txt). Assisted-by: Codex --- .github/workflows/build.yml | 13 ++++ README.md | 2 + docs/BUILDING.md | 11 +++ docs/OPENSSL-4.md | 55 ++++++++++++++ docs/UPSTREAM-26.11.md | 4 ++ docs/VERSION-LINES.md | 3 + expected-tree.txt | 2 +- mobile-src/tools/android_build.sh | 13 ++++ mobile-src/tools/android_openssl.sh | 72 +++++++++++++++++++ ...S-dest-os-and-toolchain-configure.py.patch | 33 ++++++--- 10 files changed, 199 insertions(+), 9 deletions(-) create mode 100644 docs/OPENSSL-4.md create mode 100755 mobile-src/tools/android_openssl.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f0fa572fc0..1040d6caa1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -193,6 +193,7 @@ jobs: # binary, so they must change the key. H=$( { git rev-parse "HEAD:src" "HEAD:deps" "HEAD:lib" "HEAD:tools/v8_gypfiles" "HEAD:tools/gyp" "HEAD:tools/icu"; \ git hash-object node.gyp node.gypi common.gypi configure configure.py android-configure android_configure.py tools/android_build.sh tools/copy_libnode_headers.sh; \ + if [ -f tools/android_openssl.sh ]; then git hash-object tools/android_openssl.sh; else echo no-android-openssl; fi; \ printf '%s\n' "$ANDROID_LDFLAGS" "$ANDROID_TARGET_SDK_VERSION"; \ } | git hash-object --stdin ) echo "key=rust${NODEJS_MOBILE_RUST_TOOLCHAIN}-libnode-android-${TARGET_ARCH}${FLAVOR_SUFFIX}-ndk${ANDROID_NDK_VERSION}-${H}" >> "$GITHUB_OUTPUT" @@ -248,6 +249,18 @@ jobs: run: | ./tools/android_build.sh $MY_ANDROID_NDK_HOME $ANDROID_TARGET_SDK_VERSION $TARGET_ARCH + # The Android build links a prebuilt static OpenSSL 4.x tree through + # --shared-openssl (docs/OPENSSL-4.md). Prove the shipped library carries + # it. Runs on cache hits too, where out_android is restored. + - name: Verify OpenSSL 4.x linkage + run: | + case "$TARGET_ARCH" in + arm) abi=armeabi-v7a ;; + arm64) abi=arm64-v8a ;; + x86_64) abi=x86_64 ;; + esac + grep -a -o -m1 'OpenSSL 4\.0\.[0-9]*' "out_android/$abi/libnode.so" + # NAPI/addon headers are static src/ + deps/ files (no build output, no # toolchain), so stage them unconditionally — this survives a libnode cache # hit (where the Build step is skipped) and lets combine-android just merge diff --git a/README.md b/README.md index 5480b4dac9..70011add2d 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,8 @@ This checkout pins the official tag `v26.11.1` at commit [proposal #66546](https://github.com/nodejs/node/pull/66546) snapshot to the patch release upstream published afterwards. [Provenance and validation](docs/UPSTREAM-26.11.md) record the rebase. +Android builds link a prebuilt static OpenSSL 4.0.3 tree instead of the +bundled 3.5.x line; see [Android OpenSSL 4.x](docs/OPENSSL-4.md). The stable Node 24 line continues independently on [`recipe`](https://github.com/fogtape/nodejs-mobile/tree/recipe). See [maintained version lines](docs/VERSION-LINES.md) for branch selection, build instructions and download locations. `process.version` reports the diff --git a/docs/BUILDING.md b/docs/BUILDING.md index 38a3d95b75..a8464e3e83 100644 --- a/docs/BUILDING.md +++ b/docs/BUILDING.md @@ -120,6 +120,17 @@ make # -> out/Release/lib.target/libnode.so ``` +### Android OpenSSL 4.x + +Android builds link a prebuilt static OpenSSL 4.x tree instead of the +bundled 3.5.x line. `tools/android_build.sh` fetches and builds the pinned +release asset per ABI (`tools/android_openssl.sh`, OpenSSL 4.0.3, SHA-256 +verified) and passes it to configure through +`NODEJS_MOBILE_ANDROID_OPENSSL_DIR`; the linked `libnode.so` is checked for +the embedded version string. Set `NODEJS_MOBILE_ANDROID_OPENSSL=0` to build +against the bundled line instead. iOS is unaffected. Details, rationale and +maintenance: [OPENSSL-4.md](OPENSSL-4.md). + --- ## iOS — build on macOS diff --git a/docs/OPENSSL-4.md b/docs/OPENSSL-4.md new file mode 100644 index 0000000000..18740326b9 --- /dev/null +++ b/docs/OPENSSL-4.md @@ -0,0 +1,55 @@ +# Android OpenSSL 4.x + +Android builds on this line link a prebuilt, statically linked OpenSSL 4.0.3 +tree instead of Node's bundled 3.5.x. iOS and the fallback path keep the +bundled line unchanged. + +Why: OpenSSL 4.0 is the first release with Encrypted Client Hello (ECH, +RFC 9849). Upstream Node still bundles 3.5.x and exposes no ECH API yet +([nodejs/node#64280](https://github.com/nodejs/node/issues/64280) is an open +tracking issue), so this change buys the *library* capability only — +node:tls / node:quic wiring is a separate, later step. `node:quic` stays +disabled exactly as before this change (`--experimental-quic` is not +passed); enabling it against a shared OpenSSL additionally needs the +ngtcp2/nghttp3 side to be provided and is not part of this change. + +## How it is built + +- `tools/android_openssl.sh` downloads the pinned release asset + (`openssl-4.0.3.tar.gz`), verifies its SHA-256, and configures it with the + NDK for the target ABI: + `no-shared no-module no-tests no-docs no-asm -fPIC --libdir=lib`. + `no-asm` mirrors the bundled builds this replaces (`--openssl-no-asm`). + The result is a static `libcrypto.a` / `libssl.a` tree under + `/../openssl4-android/prefix/-` (override with + `OPENSSL4_ROOT`). +- `tools/android_build.sh` builds that tree per ABI and exports + `NODEJS_MOBILE_ANDROID_OPENSSL_DIR`. +- `android_configure.py` then passes + `--shared-openssl --shared-openssl-includes=… --shared-openssl-libpath=…` + to Node's configure and drops `--openssl-no-asm` (incompatible with + `--shared-openssl`). +- The archives are linked into `libnode.so`; no new runtime `.so` files + ship with the app. + +## Verify + +- Every Android flavor/ABI build greps the artifact for the embedded + version and fails without it: + `grep -a -o -m1 'OpenSSL 4\.0\.[0-9]*' out_android//libnode.so`. + The Build workflow repeats this after restore, so cache hits are covered. +- On device, `process.versions.openssl` reports `4.0.3`. + +## Fall back to the bundled line + +```sh +NODEJS_MOBILE_ANDROID_OPENSSL=0 ./tools/android_build.sh 24 +``` + +## Maintenance + +- Bump `OPENSSL4_VERSION` and `OPENSSL4_SHA256` together in + `tools/android_openssl.sh`; the Android libnode cache key includes the + script's hash, so a stale library cannot be restored after a bump. +- Prefer the latest 4.0.x patch release; ECH API stability matters more + than new features here. diff --git a/docs/UPSTREAM-26.11.md b/docs/UPSTREAM-26.11.md index 19405e2119..1eb74a518f 100644 --- a/docs/UPSTREAM-26.11.md +++ b/docs/UPSTREAM-26.11.md @@ -9,6 +9,10 @@ patch absorbed upstream's edits to `.github/workflows/build-tarball.yml` version is `26.11.1-0`, and `expected-tree.txt` records the new source-tree checksum. +On top of this rebase, Android links a prebuilt OpenSSL 4.0.3 static tree +([OPENSSL-4.md](OPENSSL-4.md)); iOS and the bundled-OpenSSL fallback are +unchanged. + This recipe previews official Node.js release proposal [#66546](https://github.com/nodejs/node/pull/66546), titled `2026-10-07, Version 26.11.0 (Current)`. At preparation time on 2026-10-06, diff --git a/docs/VERSION-LINES.md b/docs/VERSION-LINES.md index a55cab9024..8832f27ce5 100644 --- a/docs/VERSION-LINES.md +++ b/docs/VERSION-LINES.md @@ -47,6 +47,9 @@ partition and expected source-tree checksum. The Node 26 line uses Android NDK r29 (`29.0.14206865`) and Clang 19 for Linux host tools. Node 24 keeps its existing NDK r27d configuration. +Android also links a prebuilt static OpenSSL 4.0.3 tree instead of the +bundled 3.5.x line ([OPENSSL-4.md](OPENSSL-4.md)); `node:quic` stays +disabled and no ECH API is enabled by that change alone. This Node 26.11 preview’s full builds enable V8 Temporal and the experimental `node:ffi` module on Android and iOS, with complete ICU data (`--with-intl=full-icu`) diff --git a/expected-tree.txt b/expected-tree.txt index 1fb6ba6123..b992be06e1 100644 --- a/expected-tree.txt +++ b/expected-tree.txt @@ -14,4 +14,4 @@ # run prepare.sh (or regenerate-patches.py, which prints the hash) and # update the value below in the same commit. If you forget, CI fails and # prints the hash it got. -2a6d2dfd8e605b6eb1a0a2c2c24c48fe92836295 +70b5331e853ee5c94b9501072dda8758236b9703 diff --git a/mobile-src/tools/android_build.sh b/mobile-src/tools/android_build.sh index 6efc2ffcd0..b2f030c928 100755 --- a/mobile-src/tools/android_build.sh +++ b/mobile-src/tools/android_build.sh @@ -27,6 +27,14 @@ BUILD_ARCH() { make clean rm -rf android-toolchain/ + # Android links a prebuilt static OpenSSL 4.x tree (see docs/OPENSSL-4.md + # on the recipe branch). NODEJS_MOBILE_ANDROID_OPENSSL=0 keeps the bundled + # 3.5.x path for comparison builds. + if [ "${NODEJS_MOBILE_ANDROID_OPENSSL:-1}" != "0" ]; then + NODEJS_MOBILE_ANDROID_OPENSSL_DIR=$(bash "$SCRIPT_DIR/android_openssl.sh" "$ANDROID_NDK_PATH" "$ANDROID_SDK_VERSION" "$TARGET_ARCH") + export NODEJS_MOBILE_ANDROID_OPENSSL_DIR + fi + # Compile eval '"./android-configure" "$ANDROID_NDK_PATH" $ANDROID_SDK_VERSION $TARGET_ARCH' make -j $(getconf _NPROCESSORS_ONLN) @@ -51,6 +59,11 @@ BUILD_ARCH() { echo "Could not find libnode.so file after compilation" exit 1 fi + if [ "${NODEJS_MOBILE_ANDROID_OPENSSL:-1}" != "0" ]; then + # Prove the artifact really carries the external OpenSSL line. + grep -a -o -m1 'OpenSSL 4\.0\.[0-9]*' "out_android/$TARGET_ARCH_FOLDER/libnode.so" \ + || { echo "libnode.so does not embed the external OpenSSL 4.x build" >&2; exit 1; } + fi # Keep the linked build output for debugging; only trim the copied lite SDK. if [ "${NODEJS_MOBILE_FLAVOR:-full}" = "lite" ]; then python3 tools/strip-android-runtime.py --ndk "$ANDROID_NDK_PATH" \ diff --git a/mobile-src/tools/android_openssl.sh b/mobile-src/tools/android_openssl.sh new file mode 100755 index 0000000000..687309a40a --- /dev/null +++ b/mobile-src/tools/android_openssl.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +# Build a static OpenSSL 4.x tree for one Android ABI and print its prefix. +# +# The Android nodejs-mobile build links libnode.so against this tree through +# --shared-openssl (android_configure.py reads NODEJS_MOBILE_ANDROID_OPENSSL_DIR). +# Upstream Node bundles the OpenSSL 3.5.x line; 4.x is the prerequisite for +# ECH-capable runtimes (docs/OPENSSL-4.md on the recipe branch). +# +# Usage: tools/android_openssl.sh +# +# Environment overrides: +# OPENSSL4_VERSION OpenSSL version to build (default 4.0.3) +# OPENSSL4_SHA256 Expected tarball checksum (default: 4.0.3 release asset) +# OPENSSL4_ROOT Build root (default: /../openssl4-android) +set -euo pipefail + +OPENSSL4_VERSION=${OPENSSL4_VERSION:-4.0.3} +OPENSSL4_SHA256=${OPENSSL4_SHA256:-325b5c806167c13b40b1ffeadfe0248197c00eccc4cf123ec1e28d2d2fd216d9} + +NDK=${1:?usage: android_openssl.sh } +API=${2:?usage: android_openssl.sh } +ARCH=${3:?usage: android_openssl.sh } + +case "$ARCH" in + arm) TARGET=android-arm ;; + arm64) TARGET=android-arm64 ;; + x86_64) TARGET=android-x86_64 ;; + *) echo "unsupported arch: $ARCH" >&2; exit 1 ;; +esac + +ROOT=${OPENSSL4_ROOT:-$(cd "$(dirname "$0")/../.." && pwd)/../openssl4-android} +SRC="$ROOT/src/$ARCH/openssl-$OPENSSL4_VERSION" +PREFIX="$ROOT/prefix/$ARCH-$OPENSSL4_VERSION" +STAMP="$PREFIX/.openssl-$OPENSSL4_VERSION.built" + +if [ -f "$PREFIX/lib/libssl.a" ] && [ -f "$STAMP" ]; then + echo "$PREFIX" + exit 0 +fi + +TOOLCHAIN="$NDK/toolchains/llvm/prebuilt/linux-x86_64" +[ -d "$TOOLCHAIN" ] || TOOLCHAIN="$NDK/toolchains/llvm/prebuilt/darwin-x86_64" +[ -d "$TOOLCHAIN" ] || { echo "no NDK LLVM toolchain under $NDK" >&2; exit 1; } + +mkdir -p "$ROOT/src/$ARCH" +if [ ! -d "$SRC" ]; then + TARBALL="$ROOT/openssl-$OPENSSL4_VERSION.tar.gz" + if [ ! -f "$TARBALL" ]; then + echo "Fetching OpenSSL $OPENSSL4_VERSION..." >&2 + curl -fsSL --retry 3 --retry-delay 2 -o "$TARBALL.download" \ + "https://github.com/openssl/openssl/releases/download/openssl-$OPENSSL4_VERSION/openssl-$OPENSSL4_VERSION.tar.gz" + mv "$TARBALL.download" "$TARBALL" + fi + echo "$OPENSSL4_SHA256 $TARBALL" | sha256sum -c - >&2 + tar -xzf "$TARBALL" -C "$ROOT/src/$ARCH" +fi + +cd "$SRC" +export ANDROID_NDK_ROOT="$NDK" +export PATH="$TOOLCHAIN/bin:$PATH" + +# no-asm mirrors the bundled builds this replaces; -fPIC because the static +# archives are linked into libnode.so; no-module/no-tests/no-docs keep the +# tree minimal. --libdir=lib: OpenSSL would otherwise install into lib64 on +# some hosts. +./Configure "$TARGET" -D__ANDROID_API__="$API" \ + no-shared no-module no-tests no-docs no-asm \ + -fPIC --libdir=lib --prefix="$PREFIX" --openssldir="$PREFIX/ssl" >&2 +make -j"$(getconf _NPROCESSORS_ONLN)" >&2 +make install_sw >&2 +touch "$STAMP" +echo "$PREFIX" diff --git a/patches/build-Android-iOS-dest-os-and-toolchain-configure.py.patch b/patches/build-Android-iOS-dest-os-and-toolchain-configure.py.patch index d5358b0de8..4cc911ad93 100644 --- a/patches/build-Android-iOS-dest-os-and-toolchain-configure.py.patch +++ b/patches/build-Android-iOS-dest-os-and-toolchain-configure.py.patch @@ -26,15 +26,15 @@ Co-authored-by: Vincent Breitmoser Signed-off-by: fogtape Assisted-by: Codex --- - android_configure.py | 76 +++++++++++++++++++++++++++++++++++--------- - configure.py | 8 +++++ - 2 files changed, 69 insertions(+), 15 deletions(-) + android_configure.py | 93 +++++++++++++++++++++++++++++++++++++------- + configure.py | 8 ++++ + 2 files changed, 86 insertions(+), 15 deletions(-) diff --git a/android_configure.py b/android_configure.py -index 5cea0393f4..c76ec94c8f 100644 +index 5cea0393f4..2cf132d8a0 100644 --- a/android_configure.py +++ b/android_configure.py -@@ -1,25 +1,14 @@ +@@ -1,25 +1,15 @@ import platform import sys import os @@ -46,6 +46,7 @@ index 5cea0393f4..c76ec94c8f 100644 - if platform.system() == "Linux": - os.system('patch -f ./deps/v8/src/trap-handler/trap-handler.h < ./android-patches/trap-handler.h.patch') - print("\033[92mInfo: \033[0m" + "Tried to patch.") ++import shlex +import shutil if platform.system() != "Linux" and platform.system() != "Darwin": @@ -62,7 +63,7 @@ index 5cea0393f4..c76ec94c8f 100644 sys.exit(1) if not os.path.exists(sys.argv[1]) or not os.listdir(sys.argv[1]): -@@ -55,7 +44,7 @@ else: +@@ -55,7 +45,7 @@ else: print("\033[92mInfo: \033[0m" + "Configuring for " + DEST_CPU + "...") if platform.system() == "Darwin": @@ -71,7 +72,7 @@ index 5cea0393f4..c76ec94c8f 100644 toolchain_path = android_ndk_path + "/toolchains/llvm/prebuilt/darwin-x86_64" elif platform.system() == "Linux": -@@ -65,13 +54,70 @@ elif platform.system() == "Linux": +@@ -65,13 +55,86 @@ elif platform.system() == "Linux": os.environ['PATH'] += os.pathsep + toolchain_path + "/bin" os.environ['CC'] = toolchain_path + "/bin/" + TOOLCHAIN_PREFIX + android_sdk_version + "-" + "clang" os.environ['CXX'] = toolchain_path + "/bin/" + TOOLCHAIN_PREFIX + android_sdk_version + "-" + "clang++" @@ -140,9 +141,25 @@ index 5cea0393f4..c76ec94c8f 100644 + os.environ['GYP_DEFINES'] = GYP_DEFINES ++# Android OpenSSL line selection: ++# - default: the bundled OpenSSL tree (3.5.x) with --openssl-no-asm, as before. ++# - NODEJS_MOBILE_ANDROID_OPENSSL_DIR set (tools/android_build.sh builds the ++# tree via tools/android_openssl.sh): link the prebuilt static OpenSSL 4.x ++# tree with --shared-openssl. --openssl-no-asm is a bundled-tree option and ++# is incompatible with --shared-openssl, so it is dropped on this path. ++openssl_flags = " --openssl-no-asm" ++openssl_dir = os.environ.get("NODEJS_MOBILE_ANDROID_OPENSSL_DIR", "").strip() ++if openssl_dir: ++ if not os.path.isdir(openssl_dir): ++ print("\033[91mError: \033[0m" + "NODEJS_MOBILE_ANDROID_OPENSSL_DIR is not a directory: " + openssl_dir) ++ sys.exit(1) ++ openssl_flags = (" --shared-openssl" ++ " --shared-openssl-includes=" + shlex.quote(openssl_dir + "/include") + ++ " --shared-openssl-libpath=" + shlex.quote(openssl_dir + "/lib")) ++ if os.path.exists("./configure"): - os.system("./configure --dest-cpu=" + DEST_CPU + " --dest-os=android --openssl-no-asm --cross-compiling") -+ sys.exit(os.system("./configure --dest-cpu=" + DEST_CPU + " --dest-os=android --openssl-no-asm --with-intl=" + intl + extra_flags + " --cross-compiling --shared") >> 8) ++ sys.exit(os.system("./configure --dest-cpu=" + DEST_CPU + " --dest-os=android --with-intl=" + intl + extra_flags + openssl_flags + " --cross-compiling --shared") >> 8) diff --git a/configure.py b/configure.py index acda6a16c9..c44509883d 100755 --- a/configure.py From 4eab8583028b2d596c45d36ac4190f26201b6c8d Mon Sep 17 00:00:00 2001 From: fogtape Date: Wed, 14 Oct 2026 21:26:52 +0800 Subject: [PATCH 03/16] docs: add the 26.11.1-0 release-notes section --- docs/CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index bd3a8c4850..55d0f4e30b 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -6,6 +6,7 @@ +26.11.1-0
26.11.0-0
26.10.0-0
26.1.0-0
@@ -42,6 +43,18 @@ + +## 2026-10-14, Version 26.11.1-0 (Current) + +- Rebase onto the official Node.js `v26.11.1` release tag + (`364593d0631def13e3043dd84f77ebca2c767ce5`); the regenerated series + changes only the upstream-workflow removal patch. +- Android links a prebuilt static OpenSSL 4.0.3 tree built by + `tools/android_openssl.sh` (`--shared-openssl`, SHA-256-verified release + asset) instead of the bundled 3.5.x line — the library prerequisite for + future ECH work. iOS and the bundled-OpenSSL fallback are unchanged; + `node:quic` remains disabled and no ECH API is enabled by this change. + ## 2026-10-07, Version 26.11.0-0 (Current) From f06e130e2fc7409d9763cf4d9acef70f320ab663 Mon Sep 17 00:00:00 2001 From: fogtape Date: Wed, 14 Oct 2026 21:59:00 +0800 Subject: [PATCH 04/16] android: scope external OpenSSL libraries to the node targets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit node_js2c (host) failed to link: configure put the external OpenSSL libpath and -lssl/-lcrypto into the global libraries, which gyp applies to host tool links as well, and the host linker cannot use the target ABI's archives. - android_configure.py now passes only --shared-openssl-includes and exports the library directory through GYP_DEFINES; a post-configure step drops any stray -lcrypto/-lssl from config.gypi's global libraries in case pkg-config added them. - node.gypi applies -L… -lssl -lcrypto for the node/libnode targets only (node.gypi is not included by host tools; node_mksnapshot is not built on Android). A fresh shallow reconstruction reproduces tree 5cfd8f0bf17dcca6a7a594dc272f83991a664a02 (expected-tree.txt). Assisted-by: Codex --- docs/OPENSSL-4.md | 12 ++-- expected-tree.txt | 2 +- ...S-dest-os-and-toolchain-configure.py.patch | 46 +++++++++++----- ...mobile-targets-in-node.gyp-node.gypi.patch | 55 +++++++++++++++---- 4 files changed, 87 insertions(+), 28 deletions(-) diff --git a/docs/OPENSSL-4.md b/docs/OPENSSL-4.md index 18740326b9..551f3fd3f6 100644 --- a/docs/OPENSSL-4.md +++ b/docs/OPENSSL-4.md @@ -25,10 +25,14 @@ ngtcp2/nghttp3 side to be provided and is not part of this change. `OPENSSL4_ROOT`). - `tools/android_build.sh` builds that tree per ABI and exports `NODEJS_MOBILE_ANDROID_OPENSSL_DIR`. -- `android_configure.py` then passes - `--shared-openssl --shared-openssl-includes=… --shared-openssl-libpath=…` - to Node's configure and drops `--openssl-no-asm` (incompatible with - `--shared-openssl`). +- `android_configure.py` passes `--shared-openssl --shared-openssl-includes=…` + to Node's configure (and drops `--openssl-no-asm`, incompatible with + `--shared-openssl`), and exports the library directory through + `GYP_DEFINES`. `node.gypi` adds `-L… -lssl -lcrypto` for the node/libnode + targets only: a global `--shared-openssl-libpath` would also land on the + host tools' link lines (node_js2c links first), where the Android archives + cannot be used. A post-configure step drops any stray `-lcrypto`/`-lssl` + that pkg-config may have added to the global libraries. - The archives are linked into `libnode.so`; no new runtime `.so` files ship with the app. diff --git a/expected-tree.txt b/expected-tree.txt index b992be06e1..e8dc32d719 100644 --- a/expected-tree.txt +++ b/expected-tree.txt @@ -14,4 +14,4 @@ # run prepare.sh (or regenerate-patches.py, which prints the hash) and # update the value below in the same commit. If you forget, CI fails and # prints the hash it got. -70b5331e853ee5c94b9501072dda8758236b9703 +5cfd8f0bf17dcca6a7a594dc272f83991a664a02 diff --git a/patches/build-Android-iOS-dest-os-and-toolchain-configure.py.patch b/patches/build-Android-iOS-dest-os-and-toolchain-configure.py.patch index 4cc911ad93..a3b3c3e5ff 100644 --- a/patches/build-Android-iOS-dest-os-and-toolchain-configure.py.patch +++ b/patches/build-Android-iOS-dest-os-and-toolchain-configure.py.patch @@ -26,15 +26,15 @@ Co-authored-by: Vincent Breitmoser Signed-off-by: fogtape Assisted-by: Codex --- - android_configure.py | 93 +++++++++++++++++++++++++++++++++++++------- - configure.py | 8 ++++ - 2 files changed, 86 insertions(+), 15 deletions(-) + android_configure.py | 113 +++++++++++++++++++++++++++++++++++++------ + configure.py | 8 +++ + 2 files changed, 106 insertions(+), 15 deletions(-) diff --git a/android_configure.py b/android_configure.py -index 5cea0393f4..2cf132d8a0 100644 +index 5cea0393f4..c27572e7b9 100644 --- a/android_configure.py +++ b/android_configure.py -@@ -1,25 +1,15 @@ +@@ -1,25 +1,16 @@ import platform import sys import os @@ -46,6 +46,7 @@ index 5cea0393f4..2cf132d8a0 100644 - if platform.system() == "Linux": - os.system('patch -f ./deps/v8/src/trap-handler/trap-handler.h < ./android-patches/trap-handler.h.patch') - print("\033[92mInfo: \033[0m" + "Tried to patch.") ++import json +import shlex +import shutil @@ -63,7 +64,7 @@ index 5cea0393f4..2cf132d8a0 100644 sys.exit(1) if not os.path.exists(sys.argv[1]) or not os.listdir(sys.argv[1]): -@@ -55,7 +45,7 @@ else: +@@ -55,7 +46,7 @@ else: print("\033[92mInfo: \033[0m" + "Configuring for " + DEST_CPU + "...") if platform.system() == "Darwin": @@ -72,7 +73,7 @@ index 5cea0393f4..2cf132d8a0 100644 toolchain_path = android_ndk_path + "/toolchains/llvm/prebuilt/darwin-x86_64" elif platform.system() == "Linux": -@@ -65,13 +55,86 @@ elif platform.system() == "Linux": +@@ -65,13 +56,105 @@ elif platform.system() == "Linux": os.environ['PATH'] += os.pathsep + toolchain_path + "/bin" os.environ['CC'] = toolchain_path + "/bin/" + TOOLCHAIN_PREFIX + android_sdk_version + "-" + "clang" os.environ['CXX'] = toolchain_path + "/bin/" + TOOLCHAIN_PREFIX + android_sdk_version + "-" + "clang++" @@ -139,27 +140,46 @@ index 5cea0393f4..2cf132d8a0 100644 + if arch in ("arm64", "x64"): + extra_flags += " --experimental-enable-pointer-compression" + - os.environ['GYP_DEFINES'] = GYP_DEFINES - +# Android OpenSSL line selection: +# - default: the bundled OpenSSL tree (3.5.x) with --openssl-no-asm, as before. +# - NODEJS_MOBILE_ANDROID_OPENSSL_DIR set (tools/android_build.sh builds the +# tree via tools/android_openssl.sh): link the prebuilt static OpenSSL 4.x -+# tree with --shared-openssl. --openssl-no-asm is a bundled-tree option and -+# is incompatible with --shared-openssl, so it is dropped on this path. ++# tree. Only its include directory goes through configure; the library ++# path travels in GYP_DEFINES and the -L/-l flags are applied by node.gypi ++# to the node/libnode targets only, because a global ++# --shared-openssl-libpath would also land on the host tools' link lines, ++# which cannot use the target-ABI archives (node_js2c links first). ++# --openssl-no-asm is a bundled-tree option and is incompatible with ++# --shared-openssl, so it is dropped on this path. +openssl_flags = " --openssl-no-asm" +openssl_dir = os.environ.get("NODEJS_MOBILE_ANDROID_OPENSSL_DIR", "").strip() +if openssl_dir: + if not os.path.isdir(openssl_dir): + print("\033[91mError: \033[0m" + "NODEJS_MOBILE_ANDROID_OPENSSL_DIR is not a directory: " + openssl_dir) + sys.exit(1) ++ GYP_DEFINES += " nodejs_mobile_openssl_libpath=" + openssl_dir + "/lib" + openssl_flags = (" --shared-openssl" + " --shared-openssl-includes=" + shlex.quote(openssl_dir + "/include") + -+ " --shared-openssl-libpath=" + shlex.quote(openssl_dir + "/lib")) ++ " --shared-openssl-libname=") + + os.environ['GYP_DEFINES'] = GYP_DEFINES + if os.path.exists("./configure"): - os.system("./configure --dest-cpu=" + DEST_CPU + " --dest-os=android --openssl-no-asm --cross-compiling") -+ sys.exit(os.system("./configure --dest-cpu=" + DEST_CPU + " --dest-os=android --with-intl=" + intl + extra_flags + openssl_flags + " --cross-compiling --shared") >> 8) ++ configure_status = os.system("./configure --dest-cpu=" + DEST_CPU + " --dest-os=android --with-intl=" + intl + extra_flags + openssl_flags + " --cross-compiling --shared") >> 8 ++ if configure_status == 0 and openssl_dir: ++ # Belt and braces: drop any -lcrypto/-lssl that pkg-config may have ++ # added to the global libraries (see the host-tool note above), so ++ # node.gypi's target-scoped block is the only link path to OpenSSL. ++ with open("config.gypi") as handle: ++ mobile_config = json.load(handle) ++ libraries = mobile_config.get("libraries", []) ++ kept = [lib for lib in libraries if lib not in ("-lcrypto", "-lssl")] ++ if kept != libraries: ++ mobile_config["libraries"] = kept ++ with open("config.gypi", "w") as handle: ++ json.dump(mobile_config, handle, indent=2) ++ sys.exit(configure_status) diff --git a/configure.py b/configure.py index acda6a16c9..c44509883d 100755 --- a/configure.py diff --git a/patches/build-mobile-targets-in-node.gyp-node.gypi.patch b/patches/build-mobile-targets-in-node.gyp-node.gypi.patch index 2f72b2f2e5..766eec44a9 100644 --- a/patches/build-mobile-targets-in-node.gyp-node.gypi.patch +++ b/patches/build-mobile-targets-in-node.gyp-node.gypi.patch @@ -22,15 +22,27 @@ Co-authored-by: Julia Samól Signed-off-by: fogtape Assisted-by: Codex --- - node.gyp | 18 +++++++++++++++--- - node.gypi | 22 ++++++++++++++++++---- - 2 files changed, 33 insertions(+), 7 deletions(-) + node.gyp | 23 ++++++++++++++++++++--- + node.gypi | 38 ++++++++++++++++++++++++++++++++++---- + 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/node.gyp b/node.gyp -index 587df5cace..c1d6e6735f 100644 +index 587df5cace..7fa1892d2d 100644 --- a/node.gyp +++ b/node.gyp -@@ -77,6 +77,11 @@ +@@ -28,6 +28,11 @@ + 'node_shared_nbytes%': 'false', + 'node_shared_nghttp2%': 'false', + 'node_shared_openssl%': 'false', ++ # nodejs-mobile: lib directory of an external OpenSSL tree to link into ++ # the node targets (empty = bundled OpenSSL). android_configure.py sets it ++ # through GYP_DEFINES; node.gypi applies the -L/-l flags so that host ++ # tools never see the target ABI's archives. ++ 'nodejs_mobile_openssl_libpath%': '', + 'node_shared_perfetto%': 'false', + 'node_shared_sqlite%': 'false', + 'node_shared_ffi%': 'false', +@@ -77,6 +82,11 @@ 'deps/acorn/acorn/dist/acorn.js', 'deps/acorn/acorn-walk/dist/walk.js', 'deps/minimatch/index.js', @@ -42,7 +54,7 @@ index 587df5cace..c1d6e6735f 100644 '<@(node_builtin_shareable_builtins)', ], 'node_sources': [ -@@ -580,6 +585,11 @@ +@@ -580,6 +590,11 @@ ['OS=="linux" and clang==1', { 'libraries': ['-latomic'], }], @@ -54,7 +66,7 @@ index 587df5cace..c1d6e6735f 100644 ], }, -@@ -1427,8 +1437,9 @@ +@@ -1427,8 +1442,9 @@ ['OS=="solaris"', { 'ldflags': [ '-I<(SHARED_INTERMEDIATE_DIR)' ] }], @@ -66,7 +78,7 @@ index 587df5cace..c1d6e6735f 100644 'type': 'none', }], [ 'node_shared=="true"', { -@@ -1652,7 +1663,8 @@ +@@ -1652,7 +1668,8 @@ [ 'node_shared_libuv=="false"', { 'dependencies': [ 'deps/uv/uv.gyp:libuv#host' ], }], @@ -77,7 +89,7 @@ index 587df5cace..c1d6e6735f 100644 }], [ 'debug_node=="true"', { diff --git a/node.gypi b/node.gypi -index 9eb972adc4..67d8100fa5 100644 +index 9eb972adc4..177472fd09 100644 --- a/node.gypi +++ b/node.gypi @@ -280,6 +280,11 @@ @@ -120,7 +132,30 @@ index 9eb972adc4..67d8100fa5 100644 # -force_load or --whole-archive are not applicable for # the static library [ 'force_load=="true"', { -@@ -484,5 +493,10 @@ +@@ -445,6 +454,22 @@ + ], + }], + ] ++ }, { ++ # nodejs-mobile patch: link an external OpenSSL tree (see ++ # tools/android_openssl.sh). node.gypi is included only by the ++ # node/libnode targets, so the host tools never see these target ++ # archives; a global --shared-openssl-libpath would break their ++ # link lines. The path is set through GYP_DEFINES by ++ # android_configure.py. ++ 'conditions': [ ++ [ 'nodejs_mobile_openssl_libpath!=""', { ++ 'libraries': [ ++ '-L<(nodejs_mobile_openssl_libpath)', ++ '-lssl', ++ '-lcrypto', ++ ], ++ }], ++ ], + }], + ] + }, { +@@ -484,5 +509,10 @@ }], ], }], From e750f184c1d45863bf94b85f2f5b0de3d6ded5b8 Mon Sep 17 00:00:00 2001 From: fogtape Date: Wed, 14 Oct 2026 22:14:20 +0800 Subject: [PATCH 05/16] android: keep the config.gypi banner when pruning stray OpenSSL libs config.gypi is a '# Do not edit' banner followed by JSON; json.load() from byte zero crashed every Android leg right after configure. Parse from the first '{' and rewrite while preserving the banner and the layout configure.py itself writes (verified against a synthetic file matching its writer). A fresh shallow reconstruction reproduces tree 1dd8edc4ccdce79b7a1e7a19b60e4a64aaa1cb60 (expected-tree.txt). Assisted-by: Codex --- expected-tree.txt | 2 +- ...S-dest-os-and-toolchain-configure.py.patch | 19 +++++++++++++------ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/expected-tree.txt b/expected-tree.txt index e8dc32d719..67ca41e0bc 100644 --- a/expected-tree.txt +++ b/expected-tree.txt @@ -14,4 +14,4 @@ # run prepare.sh (or regenerate-patches.py, which prints the hash) and # update the value below in the same commit. If you forget, CI fails and # prints the hash it got. -5cfd8f0bf17dcca6a7a594dc272f83991a664a02 +1dd8edc4ccdce79b7a1e7a19b60e4a64aaa1cb60 diff --git a/patches/build-Android-iOS-dest-os-and-toolchain-configure.py.patch b/patches/build-Android-iOS-dest-os-and-toolchain-configure.py.patch index a3b3c3e5ff..56c2246afb 100644 --- a/patches/build-Android-iOS-dest-os-and-toolchain-configure.py.patch +++ b/patches/build-Android-iOS-dest-os-and-toolchain-configure.py.patch @@ -26,12 +26,12 @@ Co-authored-by: Vincent Breitmoser Signed-off-by: fogtape Assisted-by: Codex --- - android_configure.py | 113 +++++++++++++++++++++++++++++++++++++------ + android_configure.py | 120 +++++++++++++++++++++++++++++++++++++------ configure.py | 8 +++ - 2 files changed, 106 insertions(+), 15 deletions(-) + 2 files changed, 113 insertions(+), 15 deletions(-) diff --git a/android_configure.py b/android_configure.py -index 5cea0393f4..c27572e7b9 100644 +index 5cea0393f4..db77bdf89d 100644 --- a/android_configure.py +++ b/android_configure.py @@ -1,25 +1,16 @@ @@ -73,7 +73,7 @@ index 5cea0393f4..c27572e7b9 100644 toolchain_path = android_ndk_path + "/toolchains/llvm/prebuilt/darwin-x86_64" elif platform.system() == "Linux": -@@ -65,13 +56,105 @@ elif platform.system() == "Linux": +@@ -65,13 +56,112 @@ elif platform.system() == "Linux": os.environ['PATH'] += os.pathsep + toolchain_path + "/bin" os.environ['CC'] = toolchain_path + "/bin/" + TOOLCHAIN_PREFIX + android_sdk_version + "-" + "clang" os.environ['CXX'] = toolchain_path + "/bin/" + TOOLCHAIN_PREFIX + android_sdk_version + "-" + "clang++" @@ -171,14 +171,21 @@ index 5cea0393f4..c27572e7b9 100644 + # Belt and braces: drop any -lcrypto/-lssl that pkg-config may have + # added to the global libraries (see the host-tool note above), so + # node.gypi's target-scoped block is the only link path to OpenSSL. ++ # config.gypi is a "do not edit" banner followed by JSON; keep the ++ # banner and the layout configure.py itself writes. + with open("config.gypi") as handle: -+ mobile_config = json.load(handle) ++ config_text = handle.read() ++ json_start = config_text.find("{") ++ if json_start < 0: ++ print("\033[91mError: \033[0m" + "config.gypi has no JSON body") ++ sys.exit(1) ++ mobile_config = json.loads(config_text[json_start:]) + libraries = mobile_config.get("libraries", []) + kept = [lib for lib in libraries if lib not in ("-lcrypto", "-lssl")] + if kept != libraries: + mobile_config["libraries"] = kept + with open("config.gypi", "w") as handle: -+ json.dump(mobile_config, handle, indent=2) ++ handle.write(config_text[:json_start] + json.dumps(mobile_config, indent=2) + "\n") + sys.exit(configure_status) diff --git a/configure.py b/configure.py index acda6a16c9..c44509883d 100755 From 44901a6b227b69352dbeac2fab85928499285fe5 Mon Sep 17 00:00:00 2001 From: fogtape Date: Wed, 14 Oct 2026 22:35:18 +0800 Subject: [PATCH 06/16] android: keep external OpenSSL libs out of the global libraries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit configure_library('openssl') put -L…/-lssl/-lcrypto into the global libraries, which gyp applies to every target; nop (a plain test binary) and node_js2c failed to link. Under NODEJS_MOBILE_ANDROID_OPENSSL_DIR, configure.py now adds only the include directory, and node.gypi keeps the link flags for the node/libnode targets. The previous config.gypi post-edit is dropped: it ran after gyp had already consumed the file. A fresh shallow reconstruction reproduces tree d41435800d43f8de4c6775704f8b477e371f93ee (expected-tree.txt). Assisted-by: Codex --- docs/OPENSSL-4.md | 14 ++-- expected-tree.txt | 2 +- ...S-dest-os-and-toolchain-configure.py.patch | 71 +++++++++---------- 3 files changed, 40 insertions(+), 47 deletions(-) diff --git a/docs/OPENSSL-4.md b/docs/OPENSSL-4.md index 551f3fd3f6..1ad2e4321c 100644 --- a/docs/OPENSSL-4.md +++ b/docs/OPENSSL-4.md @@ -26,13 +26,13 @@ ngtcp2/nghttp3 side to be provided and is not part of this change. - `tools/android_build.sh` builds that tree per ABI and exports `NODEJS_MOBILE_ANDROID_OPENSSL_DIR`. - `android_configure.py` passes `--shared-openssl --shared-openssl-includes=…` - to Node's configure (and drops `--openssl-no-asm`, incompatible with - `--shared-openssl`), and exports the library directory through - `GYP_DEFINES`. `node.gypi` adds `-L… -lssl -lcrypto` for the node/libnode - targets only: a global `--shared-openssl-libpath` would also land on the - host tools' link lines (node_js2c links first), where the Android archives - cannot be used. A post-configure step drops any stray `-lcrypto`/`-lssl` - that pkg-config may have added to the global libraries. + (and drops `--openssl-no-asm`, incompatible with `--shared-openssl`) and + exports the library directory through `GYP_DEFINES`. `configure.py`'s + `configure_library` is patched so this mode adds only the include + directory: the normal shared-openssl path would put `-L…`/`-lssl`/`-lcrypto` + into the global libraries, which gyp applies to host tools and pre-libnode + targets (`node_js2c`, `nop`). `node.gypi` adds the link flags for the + node/libnode targets themselves. - The archives are linked into `libnode.so`; no new runtime `.so` files ship with the app. diff --git a/expected-tree.txt b/expected-tree.txt index 67ca41e0bc..87cdacacff 100644 --- a/expected-tree.txt +++ b/expected-tree.txt @@ -14,4 +14,4 @@ # run prepare.sh (or regenerate-patches.py, which prints the hash) and # update the value below in the same commit. If you forget, CI fails and # prints the hash it got. -1dd8edc4ccdce79b7a1e7a19b60e4a64aaa1cb60 +d41435800d43f8de4c6775704f8b477e371f93ee diff --git a/patches/build-Android-iOS-dest-os-and-toolchain-configure.py.patch b/patches/build-Android-iOS-dest-os-and-toolchain-configure.py.patch index 56c2246afb..faa08139ea 100644 --- a/patches/build-Android-iOS-dest-os-and-toolchain-configure.py.patch +++ b/patches/build-Android-iOS-dest-os-and-toolchain-configure.py.patch @@ -26,15 +26,15 @@ Co-authored-by: Vincent Breitmoser Signed-off-by: fogtape Assisted-by: Codex --- - android_configure.py | 120 +++++++++++++++++++++++++++++++++++++------ - configure.py | 8 +++ - 2 files changed, 113 insertions(+), 15 deletions(-) + android_configure.py | 98 +++++++++++++++++++++++++++++++++++++------- + configure.py | 16 ++++++++ + 2 files changed, 99 insertions(+), 15 deletions(-) diff --git a/android_configure.py b/android_configure.py -index 5cea0393f4..db77bdf89d 100644 +index 5cea0393f4..fe0f9fd462 100644 --- a/android_configure.py +++ b/android_configure.py -@@ -1,25 +1,16 @@ +@@ -1,25 +1,15 @@ import platform import sys import os @@ -46,7 +46,6 @@ index 5cea0393f4..db77bdf89d 100644 - if platform.system() == "Linux": - os.system('patch -f ./deps/v8/src/trap-handler/trap-handler.h < ./android-patches/trap-handler.h.patch') - print("\033[92mInfo: \033[0m" + "Tried to patch.") -+import json +import shlex +import shutil @@ -64,7 +63,7 @@ index 5cea0393f4..db77bdf89d 100644 sys.exit(1) if not os.path.exists(sys.argv[1]) or not os.listdir(sys.argv[1]): -@@ -55,7 +46,7 @@ else: +@@ -55,7 +45,7 @@ else: print("\033[92mInfo: \033[0m" + "Configuring for " + DEST_CPU + "...") if platform.system() == "Darwin": @@ -73,7 +72,7 @@ index 5cea0393f4..db77bdf89d 100644 toolchain_path = android_ndk_path + "/toolchains/llvm/prebuilt/darwin-x86_64" elif platform.system() == "Linux": -@@ -65,13 +56,112 @@ elif platform.system() == "Linux": +@@ -65,13 +55,91 @@ elif platform.system() == "Linux": os.environ['PATH'] += os.pathsep + toolchain_path + "/bin" os.environ['CC'] = toolchain_path + "/bin/" + TOOLCHAIN_PREFIX + android_sdk_version + "-" + "clang" os.environ['CXX'] = toolchain_path + "/bin/" + TOOLCHAIN_PREFIX + android_sdk_version + "-" + "clang++" @@ -144,13 +143,13 @@ index 5cea0393f4..db77bdf89d 100644 +# - default: the bundled OpenSSL tree (3.5.x) with --openssl-no-asm, as before. +# - NODEJS_MOBILE_ANDROID_OPENSSL_DIR set (tools/android_build.sh builds the +# tree via tools/android_openssl.sh): link the prebuilt static OpenSSL 4.x -+# tree. Only its include directory goes through configure; the library -+# path travels in GYP_DEFINES and the -L/-l flags are applied by node.gypi -+# to the node/libnode targets only, because a global -+# --shared-openssl-libpath would also land on the host tools' link lines, -+# which cannot use the target-ABI archives (node_js2c links first). -+# --openssl-no-asm is a bundled-tree option and is incompatible with -+# --shared-openssl, so it is dropped on this path. ++# tree. configure.py adds only its include directory (patched ++# configure_library); the -L/-l flags travel through GYP_DEFINES and are ++# applied by node.gypi to the node/libnode targets, because global ++# libraries would also reach host tools and non-node targets (node_js2c ++# and nop link before libnode does). --openssl-no-asm is a bundled-tree ++# option and is incompatible with --shared-openssl, so it is dropped on ++# this path. +openssl_flags = " --openssl-no-asm" +openssl_dir = os.environ.get("NODEJS_MOBILE_ANDROID_OPENSSL_DIR", "").strip() +if openssl_dir: @@ -159,36 +158,15 @@ index 5cea0393f4..db77bdf89d 100644 + sys.exit(1) + GYP_DEFINES += " nodejs_mobile_openssl_libpath=" + openssl_dir + "/lib" + openssl_flags = (" --shared-openssl" -+ " --shared-openssl-includes=" + shlex.quote(openssl_dir + "/include") + -+ " --shared-openssl-libname=") ++ " --shared-openssl-includes=" + shlex.quote(openssl_dir + "/include")) + os.environ['GYP_DEFINES'] = GYP_DEFINES if os.path.exists("./configure"): - os.system("./configure --dest-cpu=" + DEST_CPU + " --dest-os=android --openssl-no-asm --cross-compiling") -+ configure_status = os.system("./configure --dest-cpu=" + DEST_CPU + " --dest-os=android --with-intl=" + intl + extra_flags + openssl_flags + " --cross-compiling --shared") >> 8 -+ if configure_status == 0 and openssl_dir: -+ # Belt and braces: drop any -lcrypto/-lssl that pkg-config may have -+ # added to the global libraries (see the host-tool note above), so -+ # node.gypi's target-scoped block is the only link path to OpenSSL. -+ # config.gypi is a "do not edit" banner followed by JSON; keep the -+ # banner and the layout configure.py itself writes. -+ with open("config.gypi") as handle: -+ config_text = handle.read() -+ json_start = config_text.find("{") -+ if json_start < 0: -+ print("\033[91mError: \033[0m" + "config.gypi has no JSON body") -+ sys.exit(1) -+ mobile_config = json.loads(config_text[json_start:]) -+ libraries = mobile_config.get("libraries", []) -+ kept = [lib for lib in libraries if lib not in ("-lcrypto", "-lssl")] -+ if kept != libraries: -+ mobile_config["libraries"] = kept -+ with open("config.gypi", "w") as handle: -+ handle.write(config_text[:json_start] + json.dumps(mobile_config, indent=2) + "\n") -+ sys.exit(configure_status) ++ sys.exit(os.system("./configure --dest-cpu=" + DEST_CPU + " --dest-os=android --with-intl=" + intl + extra_flags + openssl_flags + " --cross-compiling --shared") >> 8) diff --git a/configure.py b/configure.py -index acda6a16c9..c44509883d 100755 +index acda6a16c9..5ad9c1e81d 100755 --- a/configure.py +++ b/configure.py @@ -1304,6 +1304,11 @@ parser.add_argument('-C', @@ -213,3 +191,18 @@ index acda6a16c9..c44509883d 100755 if options.dest_os == 'android': o['variables']['OS'] = 'android' o['variables']['node_enable_experimentals'] = b(options.enable_all_experimentals) +@@ -2200,6 +2208,14 @@ def configure_library(lib, output, pkgname=None): + output['variables']['node_' + shared_lib] = b(getattr(options, shared_lib)) + + if getattr(options, shared_lib): ++ # nodejs-mobile: the Android build links the external OpenSSL tree from ++ # node.gypi, scoped to the node targets. Adding -L/-l here would put them ++ # into the global libraries, which gyp applies to host tools and every ++ # other target (node_js2c, nop). Only the include directory is global. ++ if lib == 'openssl' and os.environ.get('NODEJS_MOBILE_ANDROID_OPENSSL_DIR'): ++ if options.shared_openssl_includes: ++ output['include_dirs'] += [options.shared_openssl_includes] ++ return + (pkg_libs, pkg_cflags, pkg_libpath, _) = pkg_config(pkgname or lib) + + if options.__dict__[shared_lib + '_includes']: From 65c53e6fbce8be9c5e79d9069b18e559ec141bfe Mon Sep 17 00:00:00 2001 From: fogtape Date: Thu, 15 Oct 2026 08:01:34 +0800 Subject: [PATCH 07/16] docs: record the Android OpenSSL 4.0 build validation --- docs/OPENSSL-4.md | 12 ++++++++++++ docs/UPSTREAM-26.11.md | 3 ++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/OPENSSL-4.md b/docs/OPENSSL-4.md index 1ad2e4321c..c4d9097e43 100644 --- a/docs/OPENSSL-4.md +++ b/docs/OPENSSL-4.md @@ -44,6 +44,18 @@ ngtcp2/nghttp3 side to be provided and is not part of this change. The Build workflow repeats this after restore, so cache hits are covered. - On device, `process.versions.openssl` reports `4.0.3`. +## Validation record + +[Build run 38581872268](https://github.com/fogtape/nodejs-mobile/actions/runs/38581872268) +(2026-10-14, tree `d41435800d43f8de4c6775704f8b477e371f93ee`) passed all six +Android legs (full/lite × arm/arm64/x86_64) and all four iOS legs, both +Android boot smokes and both Android curated suites. The full device suites +are skipped on manual dispatches, so this is a preview gate, not a release +run. Every Android job re-verified the embedded version string, and a +downloaded `nodejs-mobile-android-lite` artifact carries `OpenSSL 4.0.3` in +all three ABIs. `node:quic` remains disabled on this line; wiring ECH into +node:tls / node:quic is the follow-up. + ## Fall back to the bundled line ```sh diff --git a/docs/UPSTREAM-26.11.md b/docs/UPSTREAM-26.11.md index 1eb74a518f..37708bb202 100644 --- a/docs/UPSTREAM-26.11.md +++ b/docs/UPSTREAM-26.11.md @@ -11,7 +11,8 @@ source-tree checksum. On top of this rebase, Android links a prebuilt OpenSSL 4.0.3 static tree ([OPENSSL-4.md](OPENSSL-4.md)); iOS and the bundled-OpenSSL fallback are -unchanged. +unchanged. Build run 38581872268 validated the combination (details in +[OPENSSL-4.md](OPENSSL-4.md)). This recipe previews official Node.js release proposal [#66546](https://github.com/nodejs/node/pull/66546), titled From b2edfbe3316f1281926805d9603f370414bb67e5 Mon Sep 17 00:00:00 2001 From: fogtape Date: Thu, 15 Oct 2026 10:05:20 +0800 Subject: [PATCH 08/16] crypto: connect OpenSSL 4 ECH and native QUIC on Android --- docs/OPENSSL-4.md | 32 +- expected-tree.txt | 2 +- mobile-src/src/crypto/crypto_ech.h | 50 +++ mobile-src/test/parallel/test-mobile-ech.js | 60 +++ .../mobile-test/curated-device-tests.txt | 2 + ...S-dest-os-and-toolchain-configure.py.patch | 4 +- ...mobile-targets-in-node.gyp-node.gypi.patch | 15 +- patches/files.map | 10 + patches/series | 1 + ...equired-ECH-for-TLS-and-QUIC-clients.patch | 381 ++++++++++++++++++ 10 files changed, 543 insertions(+), 14 deletions(-) create mode 100644 mobile-src/src/crypto/crypto_ech.h create mode 100644 mobile-src/test/parallel/test-mobile-ech.js create mode 100644 patches/src-expose-required-ECH-for-TLS-and-QUIC-clients.patch diff --git a/docs/OPENSSL-4.md b/docs/OPENSSL-4.md index c4d9097e43..0feb35d481 100644 --- a/docs/OPENSSL-4.md +++ b/docs/OPENSSL-4.md @@ -4,14 +4,30 @@ Android builds on this line link a prebuilt, statically linked OpenSSL 4.0.3 tree instead of Node's bundled 3.5.x. iOS and the fallback path keep the bundled line unchanged. -Why: OpenSSL 4.0 is the first release with Encrypted Client Hello (ECH, -RFC 9849). Upstream Node still bundles 3.5.x and exposes no ECH API yet -([nodejs/node#64280](https://github.com/nodejs/node/issues/64280) is an open -tracking issue), so this change buys the *library* capability only — -node:tls / node:quic wiring is a separate, later step. `node:quic` stays -disabled exactly as before this change (`--experimental-quic` is not -passed); enabling it against a shared OpenSSL additionally needs the -ngtcp2/nghttp3 side to be provided and is not part of this change. +OpenSSL 4 is connected to the client APIs on this branch. Android external-OpenSSL +builds enable experimental QUIC and use Node's vendored ngtcp2 OpenSSL adapter +and nghttp3; no external QUIC library or Go process is needed. iOS and the +bundled fallback still build without ECH. + +## Client API (fork extension) + +- `require('node:tls').ECH_SUPPORTED` reports compile-time ECH support. +- `tls.connect({ servername, echConfigList, ... })` accepts a binary + ECHConfigList (`Buffer` or another ArrayBufferView). A supplied config is + mandatory: certificate verification and TLS 1.3 are enforced inside OpenSSL, + and an unaccepted ECH handshake cannot become a successful JS connection. + `socket.echAccepted` and `socket.getECHStatus()` expose the outcome. +- `node:quic.connect(address, { servername, echConfigList, verifyPeer:'strict' })` + applies the same policy. `await session.opened` includes `echAccepted`. + ECH disables session-ticket 0-RTT and prevents stream creation until its + handshake succeeds. Load the module with `--experimental-quic`. +- ECH configuration discovery/TTL, protocol selection and safe retries belong + to the caller. No automatic plaintext-SNI fallback is implemented. + +`test-mobile-ech` covers input validation, unsupported bundled builds, and +rejection without application-data leakage to an ordinary TLS server. +Implementation validation is recorded separately from the earlier build below; +that build predates these APIs and does not validate ECH/H3. ## How it is built diff --git a/expected-tree.txt b/expected-tree.txt index 87cdacacff..f6800c279f 100644 --- a/expected-tree.txt +++ b/expected-tree.txt @@ -14,4 +14,4 @@ # run prepare.sh (or regenerate-patches.py, which prints the hash) and # update the value below in the same commit. If you forget, CI fails and # prints the hash it got. -d41435800d43f8de4c6775704f8b477e371f93ee +9312864cf12de5236761de8bc1402e73c82ea4e8 diff --git a/mobile-src/src/crypto/crypto_ech.h b/mobile-src/src/crypto/crypto_ech.h new file mode 100644 index 0000000000..3e6082d3df --- /dev/null +++ b/mobile-src/src/crypto/crypto_ech.h @@ -0,0 +1,50 @@ +#pragma once + +#include +#include +#include + +#if OPENSSL_VERSION_NUMBER >= 0x40000000L && !defined(OPENSSL_NO_ECH) +#include +#define NODE_HAVE_ECH 1 +#else +#define NODE_HAVE_ECH 0 +#endif + +namespace node::crypto { + +// Supplying an ECHConfigList always requires authenticated ECH. OpenSSL's +// ECH_REQUIRED alert handles rejection; neither TLS nor QUIC may retry with +// cleartext SNI. Verify inside OpenSSL as well as in JS so the outer server +// cannot authenticate retry configs using Node's permissive verify callback. +inline bool ConfigureECH(SSL* ssl, const uint8_t* config, size_t length) { +#if NODE_HAVE_ECH + if (SSL_set1_ech_config_list(ssl, config, length) != 1 || + SSL_set_min_proto_version(ssl, TLS1_3_VERSION) != 1) { + return false; + } + SSL_set_verify(ssl, SSL_VERIFY_PEER, nullptr); + return true; +#else + (void)ssl; + (void)config; + (void)length; + return false; +#endif +} + +inline int GetECHStatus(SSL* ssl) { +#if NODE_HAVE_ECH + char* inner = nullptr; + char* outer = nullptr; + const int status = SSL_ech_get1_status(ssl, &inner, &outer); + OPENSSL_free(inner); + OPENSSL_free(outer); + return status; +#else + (void)ssl; + return -103; // SSL_ECH_STATUS_NOT_CONFIGURED +#endif +} + +} // namespace node::crypto diff --git a/mobile-src/test/parallel/test-mobile-ech.js b/mobile-src/test/parallel/test-mobile-ech.js new file mode 100644 index 0000000000..592f417964 --- /dev/null +++ b/mobile-src/test/parallel/test-mobile-ech.js @@ -0,0 +1,60 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) common.skip('missing crypto'); +const assert = require('assert'); +const tls = require('tls'); +const fixtures = require('../common/fixtures'); +const { generateKeyPairSync } = require('crypto'); + +assert.strictEqual(typeof tls.ECH_SUPPORTED, 'boolean'); +assert.strictEqual(typeof tls.TLSSocket.prototype.getECHStatus, 'function'); +if (process.platform === 'android' && /^4\./.test(process.versions.openssl)) { + assert.strictEqual(tls.ECH_SUPPORTED, true); + assert.strictEqual(process.features.quic, true); +} +assert.throws(() => tls.connect({ echConfigList: 'invalid' }), + { code: 'ERR_INVALID_ARG_TYPE' }); +assert.throws(() => tls.connect({ echConfigList: Buffer.alloc(0) }), + { code: 'ERR_INVALID_ARG_VALUE' }); + +if (!tls.ECH_SUPPORTED) { + assert.throws(() => tls.connect({ + servername: 'localhost', echConfigList: Buffer.from([0, 0]), + }), { code: 'ERR_INVALID_ARG_VALUE' }); +} else { + // RFC 9849 ECHConfigList with a fresh public key. The ordinary local TLS + // server has no ECH key: the client MUST fail without sending application + // bytes, even when the caller queues a write before secureConnect. + const { publicKey } = generateKeyPairSync('x25519'); + const key = publicKey.export({ type: 'spki', format: 'der' }).subarray(-32); + const name = Buffer.from('localhost'); + const content = Buffer.concat([ + Buffer.from([1, 0, 32, 0, 32]), key, + Buffer.from([0, 4, 0, 1, 0, 1, 0, name.length]), name, + Buffer.from([0, 0]), + ]); + const config = Buffer.concat([ + Buffer.from([0, content.length + 4, 0xfe, 0x0d, 0, content.length]), content, + ]); + assert.throws(() => tls.connect({ servername: 'localhost', + rejectUnauthorized: false, echConfigList: config }), + { code: 'ERR_INVALID_ARG_VALUE' }); + const server = tls.createServer({ + key: fixtures.readKey('agent1-key.pem'), + cert: fixtures.readKey('agent1-cert.pem'), + }, (socket) => { + socket.on('data', common.mustNotCall('ECH rejection leaked application data')); + socket.on('error', () => {}); + }); + server.on('tlsClientError', () => {}); + server.listen(0, '127.0.0.1', common.mustCall(() => { + const socket = tls.connect({ + host: '127.0.0.1', port: server.address().port, servername: 'localhost', + ca: fixtures.readKey('agent1-cert.pem'), echConfigList: config, + }, common.mustNotCall('ECH rejection emitted secureConnect')); + socket.write('must stay encrypted to the intended ECH server', () => {}); + socket.once('error', common.mustCall(() => server.close())); + socket.setTimeout(5000, () => socket.destroy(new Error('ECH test timed out'))); + })); +} diff --git a/mobile-src/tools/mobile-test/curated-device-tests.txt b/mobile-src/tools/mobile-test/curated-device-tests.txt index f067800772..aaa7916268 100644 --- a/mobile-src/tools/mobile-test/curated-device-tests.txt +++ b/mobile-src/tools/mobile-test/curated-device-tests.txt @@ -231,3 +231,5 @@ parallel/test-zlib-decode-strings parallel/test-zlib-reset-incomplete-output parallel/test-zlib-brotli-reset parallel/test-zlib-zstd-reset-incomplete-frame + +parallel/test-mobile-ech diff --git a/patches/build-Android-iOS-dest-os-and-toolchain-configure.py.patch b/patches/build-Android-iOS-dest-os-and-toolchain-configure.py.patch index faa08139ea..d32d604cbc 100644 --- a/patches/build-Android-iOS-dest-os-and-toolchain-configure.py.patch +++ b/patches/build-Android-iOS-dest-os-and-toolchain-configure.py.patch @@ -31,7 +31,7 @@ Assisted-by: Codex 2 files changed, 99 insertions(+), 15 deletions(-) diff --git a/android_configure.py b/android_configure.py -index 5cea0393f4..fe0f9fd462 100644 +index 5cea0393f4..b1a1c6f827 100644 --- a/android_configure.py +++ b/android_configure.py @@ -1,25 +1,15 @@ @@ -157,7 +157,7 @@ index 5cea0393f4..fe0f9fd462 100644 + print("\033[91mError: \033[0m" + "NODEJS_MOBILE_ANDROID_OPENSSL_DIR is not a directory: " + openssl_dir) + sys.exit(1) + GYP_DEFINES += " nodejs_mobile_openssl_libpath=" + openssl_dir + "/lib" -+ openssl_flags = (" --shared-openssl" ++ openssl_flags = (" --experimental-quic --shared-openssl" + " --shared-openssl-includes=" + shlex.quote(openssl_dir + "/include")) + os.environ['GYP_DEFINES'] = GYP_DEFINES diff --git a/patches/build-mobile-targets-in-node.gyp-node.gypi.patch b/patches/build-mobile-targets-in-node.gyp-node.gypi.patch index 766eec44a9..98312e592f 100644 --- a/patches/build-mobile-targets-in-node.gyp-node.gypi.patch +++ b/patches/build-mobile-targets-in-node.gyp-node.gypi.patch @@ -23,8 +23,8 @@ Signed-off-by: fogtape Assisted-by: Codex --- node.gyp | 23 ++++++++++++++++++++--- - node.gypi | 38 ++++++++++++++++++++++++++++++++++---- - 2 files changed, 54 insertions(+), 7 deletions(-) + node.gypi | 40 +++++++++++++++++++++++++++++++++++----- + 2 files changed, 55 insertions(+), 8 deletions(-) diff --git a/node.gyp b/node.gyp index 587df5cace..7fa1892d2d 100644 @@ -89,7 +89,7 @@ index 587df5cace..7fa1892d2d 100644 }], [ 'debug_node=="true"', { diff --git a/node.gypi b/node.gypi -index 9eb972adc4..177472fd09 100644 +index 9eb972adc4..42015a4773 100644 --- a/node.gypi +++ b/node.gypi @@ -280,6 +280,11 @@ @@ -155,6 +155,15 @@ index 9eb972adc4..177472fd09 100644 }], ] }, { +@@ -472,7 +497,7 @@ + }], + [ 'node_use_quic=="true"', { + 'conditions': [ +- [ 'node_shared_openssl=="false"', { ++ [ 'node_shared_openssl=="false" or (OS=="android" and nodejs_mobile_openssl_libpath!="")', { + 'dependencies': [ + './deps/ngtcp2/ngtcp2.gyp:ngtcp2', + './deps/ngtcp2/ngtcp2.gyp:nghttp3', @@ -484,5 +509,10 @@ }], ], diff --git a/patches/files.map b/patches/files.map index 219ba50d38..a26a812172 100644 --- a/patches/files.map +++ b/patches/files.map @@ -138,3 +138,13 @@ build-profile-ICU-data-for-danmu_api-lite-runtimes.patch deps/icu-small/source/t build-profile-ICU-data-for-danmu_api-lite-runtimes.patch tools/icu/danmu-lite.json build-profile-ICU-data-for-danmu_api-lite-runtimes.patch tools/icu/icu-generic.gyp build-profile-ICU-data-for-danmu_api-lite-runtimes.patch tools/icu/prepare-mobile-icu.py +src-expose-required-ECH-for-TLS-and-QUIC-clients.patch deps/ngtcp2/ngtcp2.gyp +src-expose-required-ECH-for-TLS-and-QUIC-clients.patch lib/internal/quic/quic.js +src-expose-required-ECH-for-TLS-and-QUIC-clients.patch lib/internal/tls/wrap.js +src-expose-required-ECH-for-TLS-and-QUIC-clients.patch lib/tls.js +src-expose-required-ECH-for-TLS-and-QUIC-clients.patch src/crypto/crypto_tls.cc +src-expose-required-ECH-for-TLS-and-QUIC-clients.patch src/crypto/crypto_tls.h +src-expose-required-ECH-for-TLS-and-QUIC-clients.patch src/quic/bindingdata.h +src-expose-required-ECH-for-TLS-and-QUIC-clients.patch src/quic/session.cc +src-expose-required-ECH-for-TLS-and-QUIC-clients.patch src/quic/tlscontext.cc +src-expose-required-ECH-for-TLS-and-QUIC-clients.patch src/quic/tlscontext.h diff --git a/patches/series b/patches/series index 6ce282ba2b..3448b8fe88 100644 --- a/patches/series +++ b/patches/series @@ -20,3 +20,4 @@ ci-remove-upstream-only-workflows-and-config.patch lib-install-a-WebAssembly-polyfill-when-the-engine-h.patch build-mobile-Temporal-Rust-toolsets-and-libffi-iOS-c.patch build-profile-ICU-data-for-danmu_api-lite-runtimes.patch +src-expose-required-ECH-for-TLS-and-QUIC-clients.patch diff --git a/patches/src-expose-required-ECH-for-TLS-and-QUIC-clients.patch b/patches/src-expose-required-ECH-for-TLS-and-QUIC-clients.patch new file mode 100644 index 0000000000..057acdd589 --- /dev/null +++ b/patches/src-expose-required-ECH-for-TLS-and-QUIC-clients.patch @@ -0,0 +1,381 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: fogtape +Date: Thu, 15 Oct 2026 12:00:00 +0800 +Subject: [PATCH] src: expose required ECH for TLS and QUIC clients + +Expose echConfigList and authenticated acceptance without plaintext-SNI +fallback. Use OpenSSL native peer verification for ECH, suppress QUIC +0-RTT, and build the vendored QUIC adapter with Android external OpenSSL. + +Assisted-by: Codex +--- + deps/ngtcp2/ngtcp2.gyp | 2 +- + lib/internal/quic/quic.js | 15 ++++++++++++--- + lib/internal/tls/wrap.js | 29 +++++++++++++++++++++++++++++ + lib/tls.js | 3 +++ + src/crypto/crypto_tls.cc | 27 +++++++++++++++++++++++++++ + src/crypto/crypto_tls.h | 2 ++ + src/quic/bindingdata.h | 1 + + src/quic/session.cc | 8 +++++++- + src/quic/tlscontext.cc | 26 +++++++++++++++++++++++++- + src/quic/tlscontext.h | 4 ++++ + 10 files changed, 111 insertions(+), 6 deletions(-) + +diff --git a/deps/ngtcp2/ngtcp2.gyp b/deps/ngtcp2/ngtcp2.gyp +index 7ae6279248..3dcd7278a1 100644 +--- a/deps/ngtcp2/ngtcp2.gyp ++++ b/deps/ngtcp2/ngtcp2.gyp +@@ -169,7 +169,7 @@ + # here then we can use that adapter and also set the + # QUIC_NGTCP2_USE_BORINGSSL define (the guard in quic/guard.h + # would need to be updated to check for this define). +- ['node_shared_openssl=="false" and openssl_version >= 0x3050001f', { ++ ['(node_shared_openssl=="false" or (OS=="android" and nodejs_mobile_openssl_libpath!="")) and openssl_version >= 0x3050001f', { + 'sources': [ + '<@(ngtcp2_sources_ossl)', + ], +diff --git a/lib/internal/quic/quic.js b/lib/internal/quic/quic.js +index 4922ce5627..9ae28a6f48 100644 +--- a/lib/internal/quic/quic.js ++++ b/lib/internal/quic/quic.js +@@ -822,13 +822,13 @@ setCallbacks({ + validationErrorReason, + validationErrorCode, + earlyDataAttempted, +- earlyDataAccepted) { ++ earlyDataAccepted, echAccepted) { + debug('session handshake callback', servername, protocol, cipher, cipherVersion, + validationErrorReason, validationErrorCode, + earlyDataAttempted, earlyDataAccepted); + this[kOwner][kHandshake](servername, protocol, cipher, cipherVersion, + validationErrorReason, validationErrorCode, +- earlyDataAttempted, earlyDataAccepted); ++ earlyDataAttempted, earlyDataAccepted, echAccepted); + }, + + /** +@@ -4209,7 +4209,7 @@ class QuicSession { + * @param {number} validationErrorCode + */ + [kHandshake](servername, protocol, cipher, cipherVersion, validationErrorReason, +- validationErrorCode, earlyDataAttempted, earlyDataAccepted) { ++ validationErrorCode, earlyDataAttempted, earlyDataAccepted, echAccepted) { + const inner = this.#inner; + if (this.destroyed || !inner.pendingOpen.resolve) return; + +@@ -4228,6 +4228,7 @@ class QuicSession { + cipherVersion, + validationErrorReason, + validationErrorCode, ++ echAccepted, + earlyDataAttempted, + earlyDataAccepted, + }; +@@ -5231,6 +5232,7 @@ function processIdentityOptions(identity, label) { + function processTlsOptions(tls, forServer) { + const { + servername, ++ echConfigList, + alpn, + ciphers = DEFAULT_CIPHERS, + groups = DEFAULT_GROUPS, +@@ -5252,6 +5254,12 @@ function processTlsOptions(tls, forServer) { + if (servername !== undefined) { + validateString(servername, 'options.servername'); + } ++ if (echConfigList !== undefined) { ++ if (!isArrayBufferView(echConfigList)) ++ throw new ERR_INVALID_ARG_TYPE('options.echConfigList', 'ArrayBufferView', echConfigList); ++ if (forServer || !servername || echConfigList.byteLength === 0 || tls.verifyPeer === 'manual') ++ throw new ERR_INVALID_ARG_VALUE('options.echConfigList', undefined, 'ECH requires a client servername and certificate verification'); ++ } + if (ciphers !== undefined) { + validateString(ciphers, 'options.ciphers'); + } +@@ -5364,6 +5372,7 @@ function processTlsOptions(tls, forServer) { + const shared = { + __proto__: null, + servername, ++ echConfigList, + alpn: encodedAlpn, + ciphers, + groups, +diff --git a/lib/internal/tls/wrap.js b/lib/internal/tls/wrap.js +index 60c1634b02..fdad8a9821 100644 +--- a/lib/internal/tls/wrap.js ++++ b/lib/internal/tls/wrap.js +@@ -1770,6 +1770,16 @@ function normalizeConnectArgs(listArgs) { + return cb ? [options, cb] : [options]; + } + ++TLSSocket.prototype.getECHStatus = function() { ++ return this._handle?.getECHStatus(); ++}; ++ ++ObjectDefineProperty(TLSSocket.prototype, 'echAccepted', { ++ configurable: true, ++ enumerable: true, ++ get() { return this.getECHStatus() === 1; }, ++}); ++ + function onConnectSecure() { + const options = this[kConnectOptions]; + +@@ -1819,6 +1829,10 @@ function onConnectSecure() { + this.authorized = true; + debug('client emit secureConnect. authorized:', this.authorized); + } ++ if (options.echConfigList !== undefined && !this.echAccepted) { ++ this.destroy(new ERR_TLS_INVALID_STATE('ECH was not accepted')); ++ return; ++ } + this.secureConnecting = false; + this.emit('secureConnect'); + +@@ -1868,6 +1882,15 @@ exports.connect = function connect(...args) { + validateFunction(options.checkServerIdentity, 'options.checkServerIdentity'); + validateNumber(options.minDHSize, 'options.minDHSize', 1); + ++ if (options.echConfigList !== undefined) { ++ if (!isArrayBufferView(options.echConfigList)) ++ throw new ERR_INVALID_ARG_TYPE('options.echConfigList', 'ArrayBufferView', options.echConfigList); ++ if (!tls.ECH_SUPPORTED || options.echConfigList.byteLength === 0) ++ throw new ERR_INVALID_ARG_VALUE('options.echConfigList', undefined, 'ECH requires OpenSSL 4 and a nonempty ECHConfigList'); ++ if (!options.servername || options.rejectUnauthorized === false) ++ throw new ERR_INVALID_ARG_VALUE('options.echConfigList', undefined, 'ECH requires servername and certificate verification'); ++ } ++ + const context = options.secureContext || tls.createSecureContext(options); + + if (options.servername && net.isIP(options.servername)) { +@@ -1918,6 +1941,12 @@ exports.connect = function connect(...args) { + if (options.servername) + tlssock.setServername(options.servername); + ++ if (options.echConfigList !== undefined && ++ !tlssock._handle.setECHConfigList(options.echConfigList)) { ++ tlssock.destroy(); ++ throw new ERR_INVALID_ARG_VALUE('options.echConfigList', undefined, 'Invalid ECHConfigList'); ++ } ++ + if (!options.socket) { + // If user provided the socket, it's their responsibility to manage its + // connectivity. If we created one internally, we connect it. +diff --git a/lib/tls.js b/lib/tls.js +index a1d978c50f..bcc83eec30 100644 +--- a/lib/tls.js ++++ b/lib/tls.js +@@ -21,6 +21,9 @@ + + 'use strict'; + ++// Fork API: library support, independent of any server's ECH policy. ++exports.ECH_SUPPORTED = !!internalBinding('tls_wrap').NODE_HAVE_ECH; ++ + const { + Array, + ArrayIsArray, +diff --git a/src/crypto/crypto_tls.cc b/src/crypto/crypto_tls.cc +index fbc5fc8c39..c1633ee186 100644 +--- a/src/crypto/crypto_tls.cc ++++ b/src/crypto/crypto_tls.cc +@@ -25,6 +25,7 @@ + #include "crypto/crypto_bio.h" + #include "crypto/crypto_client_hello.h" + #include "crypto/crypto_common.h" ++#include "crypto/crypto_ech.h" + #include "crypto/crypto_context.h" + #include "crypto/crypto_util.h" + #include "debug_utils-inl.h" +@@ -1511,6 +1512,27 @@ void TLSWrap::GetServername(const FunctionCallbackInfo& args) { + } + } + ++void TLSWrap::SetECHConfigList(const FunctionCallbackInfo& args) { ++ TLSWrap* wrap; ++ ASSIGN_OR_RETURN_UNWRAP(&wrap, args.This()); ++ CHECK_EQ(args.Length(), 1); ++ CHECK(args[0]->IsArrayBufferView()); ++ CHECK(!wrap->flags_.started); ++ CHECK(wrap->is_client()); ++ CHECK(wrap->ssl_); ++ ArrayBufferViewContents config(args[0]); ++ args.GetReturnValue().Set(crypto::ConfigureECH( ++ wrap->ssl_.get(), config.data(), config.length())); ++} ++ ++void TLSWrap::GetECHStatus(const FunctionCallbackInfo& args) { ++ TLSWrap* wrap; ++ ASSIGN_OR_RETURN_UNWRAP(&wrap, args.This()); ++ if (wrap->ssl_) { ++ args.GetReturnValue().Set(crypto::GetECHStatus(wrap->ssl_.get())); ++ } ++} ++ + void TLSWrap::SetServername(const FunctionCallbackInfo& args) { + TLSWrap* wrap; + ASSIGN_OR_RETURN_UNWRAP(&wrap, args.This()); +@@ -2346,6 +2368,7 @@ void TLSWrap::Initialize( + SetMethod(context, target, "wrap", TLSWrap::Wrap); + + NODE_DEFINE_CONSTANT(target, HAVE_SSL_TRACE); ++ NODE_DEFINE_CONSTANT(target, NODE_HAVE_ECH); + + Local t = AsyncWrap::MakeLazilyInitializedJSTemplate(env); + Local tlsWrapString = +@@ -2384,6 +2407,8 @@ void TLSWrap::Initialize( + SetProtoMethod(isolate, t, "setKeyCert", SetKeyCert); + SetProtoMethod(isolate, t, "setOCSPResponse", SetOCSPResponse); + SetProtoMethod(isolate, t, "setServername", SetServername); ++ SetProtoMethod(isolate, t, "setECHConfigList", SetECHConfigList); ++ SetProtoMethod(isolate, t, "getECHStatus", GetECHStatus); + SetProtoMethod(isolate, t, "setSession", SetSession); + SetProtoMethod(isolate, t, "setVerifyMode", SetVerifyMode); + SetProtoMethod(isolate, t, "start", Start); +@@ -2456,6 +2481,8 @@ void TLSWrap::RegisterExternalReferences(ExternalReferenceRegistry* registry) { + registry->Register(SetALPNProtocols); + registry->Register(SetOCSPResponse); + registry->Register(SetServername); ++ registry->Register(SetECHConfigList); ++ registry->Register(GetECHStatus); + registry->Register(SetSession); + registry->Register(SetVerifyMode); + registry->Register(Start); +diff --git a/src/crypto/crypto_tls.h b/src/crypto/crypto_tls.h +index ab60240188..23b80c2154 100644 +--- a/src/crypto/crypto_tls.h ++++ b/src/crypto/crypto_tls.h +@@ -277,6 +277,8 @@ class TLSWrap : public AsyncWrap, + static void SetKeyCert(const v8::FunctionCallbackInfo& args); + static void SetOCSPResponse(const v8::FunctionCallbackInfo& args); + static void SetServername(const v8::FunctionCallbackInfo& args); ++ static void SetECHConfigList(const v8::FunctionCallbackInfo& args); ++ static void GetECHStatus(const v8::FunctionCallbackInfo& args); + static void SetSession(const v8::FunctionCallbackInfo& args); + static void SetVerifyMode(const v8::FunctionCallbackInfo& args); + static void Start(const v8::FunctionCallbackInfo& args); +diff --git a/src/quic/bindingdata.h b/src/quic/bindingdata.h +index 2ef9f76853..037bb56e8a 100644 +--- a/src/quic/bindingdata.h ++++ b/src/quic/bindingdata.h +@@ -90,6 +90,7 @@ class SessionManager; + V(disable_stateless_reset, "disableStatelessReset") \ + V(draining_period_multiplier, "drainingPeriodMultiplier") \ + V(enable_connect_protocol, "enableConnectProtocol") \ ++ V(ech_config_list, "echConfigList") \ + V(enable_early_data, "enableEarlyData") \ + V(enable_datagrams, "enableDatagrams") \ + V(enable_tls_trace, "tlsTrace") \ +diff --git a/src/quic/session.cc b/src/quic/session.cc +index d83c1cbb89..e3257e40aa 100644 +--- a/src/quic/session.cc ++++ b/src/quic/session.cc +@@ -3845,6 +3845,11 @@ bool Session::HandshakeCompleted() { + DCHECK(!is_destroyed()); + DCHECK(!impl_->state()->handshake_completed); + ++ if (!is_server() && tls_session().context().options().ech_config_list && ++ !tls_session().ech_accepted()) { ++ return false; ++ } ++ + Debug(this, "Session handshake completed"); + impl_->state()->handshake_completed = 1; + // This implies fully completing a handshake without setting hello_processed +@@ -4127,7 +4132,8 @@ void Session::EmitHandshakeComplete() { + Undefined(isolate), // Validation error reason + Undefined(isolate), // Validation error code + Boolean::New(isolate, tls_session().early_data_was_attempted()), +- Boolean::New(isolate, tls_session().early_data_was_accepted())}; ++ Boolean::New(isolate, tls_session().early_data_was_accepted()), ++ Boolean::New(isolate, tls_session().ech_accepted())}; + + auto& tls = tls_session(); + auto peerVerifyError = tls.VerifyPeerIdentity(env()); +diff --git a/src/quic/tlscontext.cc b/src/quic/tlscontext.cc +index b5106d27e1..82a5d40946 100644 +--- a/src/quic/tlscontext.cc ++++ b/src/quic/tlscontext.cc +@@ -3,6 +3,7 @@ + #include + #include + #include ++#include + #include + #include + #include +@@ -791,6 +792,14 @@ Maybe TLSContext::Options::From(Environment* env, + return Nothing(); + } + ++ Local ech; ++ if (!params->Get(env->context(), state.ech_config_list_string()).ToLocal(&ech)) ++ return Nothing(); ++ if (!ech->IsUndefined()) { ++ if (!ech->IsArrayBufferView() || ++ !Store::From(ech.As()).To(&options.ech_config_list)) ++ return Nothing(); ++ } + return Just(options); + } + +@@ -831,6 +840,7 @@ void TLSContext::Options::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("certs", certs); + tracker->TrackField("ca", ca); + tracker->TrackField("crl", crl); ++ tracker->TrackField("ech_config_list", ech_config_list); + } + + const TLSContext::Options TLSContext::Options::kDefault = {}; +@@ -932,7 +942,17 @@ void TLSSession::Initialize( + } + } + +- if (maybeSessionTicket.has_value()) { ++ if (options.ech_config_list) { ++ const ngtcp2_vec config = options.ech_config_list; ++ if (!crypto::ConfigureECH(ossl_context_, config.base, config.len)) { ++ validation_error_ = "ECH requires OpenSSL 4 and a valid ECHConfigList"; ++ ossl_context_.reset(); ++ return; ++ } ++ } ++ ++ // ECH must be authenticated before application data, including 0-RTT. ++ if (!options.ech_config_list && maybeSessionTicket.has_value()) { + const auto& sessionTicket = *maybeSessionTicket; + uv_buf_t buf = sessionTicket.ticket(); + SSLSessionPointer ticket = crypto::GetTLSSession( +@@ -1003,6 +1023,10 @@ TLSSession::VerifyPeerIdentity(Environment* env) { + return PeerIdentityValidationError{reason, code}; + } + ++bool TLSSession::ech_accepted() const { ++ return crypto::GetECHStatus(ossl_context_) == 1; ++} ++ + MaybeLocal TLSSession::cert(Environment* env) const { + SSLPointerRef ssl(ossl_context_); + return crypto::X509Certificate::GetCert(env, ssl); +diff --git a/src/quic/tlscontext.h b/src/quic/tlscontext.h +index 8209eda12d..902afa0564 100644 +--- a/src/quic/tlscontext.h ++++ b/src/quic/tlscontext.h +@@ -110,6 +110,7 @@ class TLSSession final : public MemoryRetainer { + // accepted by the TLS session. This will assert if the handshake has + // not been completed. + bool early_data_was_accepted() const; ++ bool ech_accepted() const; + bool early_data_was_rejected() const; + bool early_data_was_attempted() const; + +@@ -241,6 +242,9 @@ class TLSContext final : public MemoryRetainer, + // When true, enables keylog output for the session. + bool keylog = false; + ++ // Client-only, copied ECHConfigList. Configured ECH is mandatory. ++ Store ech_config_list; ++ + // When true, the peer certificate is verified against the list of supplied + // CA. If verification fails, the connection will be refused. When set, + // instructs the server session to request a client auth certificate. This From e6bb4b2d2d17d1a5e8d461a83b975626a838f5b9 Mon Sep 17 00:00:00 2001 From: fogtape Date: Thu, 15 Oct 2026 10:10:18 +0800 Subject: [PATCH 09/16] build: keep desktop QUIC test targets out of Android OpenSSL 4 builds --- expected-tree.txt | 2 +- ...mobile-targets-in-node.gyp-node.gypi.patch | 22 ++++++++++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/expected-tree.txt b/expected-tree.txt index f6800c279f..1da1c08e8d 100644 --- a/expected-tree.txt +++ b/expected-tree.txt @@ -14,4 +14,4 @@ # run prepare.sh (or regenerate-patches.py, which prints the hash) and # update the value below in the same commit. If you forget, CI fails and # prints the hash it got. -9312864cf12de5236761de8bc1402e73c82ea4e8 +7d3d6efed37ff4a08c95bb0301d6fd8fde0c56e4 diff --git a/patches/build-mobile-targets-in-node.gyp-node.gypi.patch b/patches/build-mobile-targets-in-node.gyp-node.gypi.patch index 98312e592f..fb19d056e1 100644 --- a/patches/build-mobile-targets-in-node.gyp-node.gypi.patch +++ b/patches/build-mobile-targets-in-node.gyp-node.gypi.patch @@ -23,8 +23,8 @@ Signed-off-by: fogtape Assisted-by: Codex --- node.gyp | 23 ++++++++++++++++++++--- - node.gypi | 40 +++++++++++++++++++++++++++++++++++----- - 2 files changed, 55 insertions(+), 8 deletions(-) + node.gypi | 51 +++++++++++++++++++++++++++++++++++++++++++-------- + 2 files changed, 63 insertions(+), 11 deletions(-) diff --git a/node.gyp b/node.gyp index 587df5cace..7fa1892d2d 100644 @@ -89,7 +89,7 @@ index 587df5cace..7fa1892d2d 100644 }], [ 'debug_node=="true"', { diff --git a/node.gypi b/node.gypi -index 9eb972adc4..42015a4773 100644 +index 9eb972adc4..56a8f45516 100644 --- a/node.gypi +++ b/node.gypi @@ -280,6 +280,11 @@ @@ -155,7 +155,7 @@ index 9eb972adc4..42015a4773 100644 }], ] }, { -@@ -472,7 +497,7 @@ +@@ -472,17 +497,27 @@ }], [ 'node_use_quic=="true"', { 'conditions': [ @@ -164,7 +164,19 @@ index 9eb972adc4..42015a4773 100644 'dependencies': [ './deps/ngtcp2/ngtcp2.gyp:ngtcp2', './deps/ngtcp2/ngtcp2.gyp:nghttp3', -@@ -484,5 +509,10 @@ + +- # For tests +- './deps/ngtcp2/ngtcp2.gyp:ngtcp2_test_server', +- './deps/ngtcp2/ngtcp2.gyp:ngtcp2_test_client', ++ ], ++ 'conditions': [ ++ [ 'OS not in "android ios"', { ++ 'dependencies': [ ++ './deps/ngtcp2/ngtcp2.gyp:ngtcp2_test_server', ++ './deps/ngtcp2/ngtcp2.gyp:ngtcp2_test_client', ++ ], ++ }], + ], }], ], }], From 7081e0a28d452b4eddb56c50e67b8f7f846b5843 Mon Sep 17 00:00:00 2001 From: fogtape Date: Thu, 15 Oct 2026 10:42:41 +0800 Subject: [PATCH 10/16] quic: fix Initial packet clock and add Android ECH H3 validation --- .github/workflows/openssl4-android.yml | 132 ++++++++++++++++++ docs/CHANGELOG.md | 8 +- docs/OPENSSL-4.md | 4 +- docs/VERSION-LINES.md | 5 +- expected-tree.txt | 2 +- mobile-src/test/parallel/test-mobile-ech.js | 2 +- mobile-src/tools/mobile-test/smoke/quic-h3.js | 57 ++++++++ .../smoke/run-android-quic-smoke.sh | 23 +++ patches/files.map | 1 + patches/series | 1 + ...-receive-time-after-session-creation.patch | 39 ++++++ 11 files changed, 265 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/openssl4-android.yml create mode 100644 mobile-src/tools/mobile-test/smoke/quic-h3.js create mode 100755 mobile-src/tools/mobile-test/smoke/run-android-quic-smoke.sh create mode 100644 patches/src-fix-QUIC-receive-time-after-session-creation.patch diff --git a/.github/workflows/openssl4-android.yml b/.github/workflows/openssl4-android.yml new file mode 100644 index 0000000000..802166f0bb --- /dev/null +++ b/.github/workflows/openssl4-android.yml @@ -0,0 +1,132 @@ +name: OpenSSL 4 Android client + +on: + push: + branches: [openssl4/node26.11.1] + paths: [patches/**, mobile-src/**, expected-tree.txt, .github/workflows/openssl4-android.yml] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: openssl4-android-${{ github.ref }} + cancel-in-progress: false + +env: + NODEJS_MOBILE_FLAVOR: lite + SCCACHE_IDLE_TIMEOUT: '0' + SCCACHE_LOG: warn + +jobs: + build: + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + arch: [arm64, arm, x86_64] + env: + LDFLAGS: -Wl,-z,max-page-size=16384 + steps: + - uses: actions/checkout@v7 + - uses: ./.github/actions/materialize + - name: Install build tools + run: | + sudo apt-get update + sudo apt-get install -y gcc-multilib g++-multilib clang-19 + echo CC_host=clang-19 >> "$GITHUB_ENV" + echo CXX_host=clang++-19 >> "$GITHUB_ENV" + - uses: actions/setup-python@v6 + with: + python-version: '3.13' + - run: python -m pip install setuptools packaging + - uses: nttld/setup-ndk@ed92fe6cadad69be94a966a7ee3271275e62f779 + id: ndk + with: + ndk-version: r29 + add-to-path: false + - uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba + - uses: ./.github/actions/restore-compiler-cache + id: objects + with: + lane: android-${{ matrix.arch }}-lite-ndkr29 + # This development workflow only uses the local cache. No R2 credentials, + # tags, releases or publication permissions enter this job. + - name: Build OpenSSL 4 and native QUIC + env: + NODEJS_MOBILE_SCCACHE: '1' + NDK: ${{ steps.ndk.outputs.ndk-path }} + ARCH: ${{ matrix.arch }} + run: | + ./tools/android_build.sh "$NDK" 24 "$ARCH" + ./tools/copy_libnode_headers.sh android + python tools/strip-android-runtime.py --ndk "$NDK" out_android + - name: Flush compiler objects + if: always() && steps.objects.outcome == 'success' + run: sccache --stop-server || true + - uses: actions/cache/save@v6 + if: always() && steps.objects.outcome == 'success' + with: + path: ${{ runner.temp }}/nodejs-mobile-sccache + key: ${{ steps.objects.outputs.key }} + - uses: actions/upload-artifact@v7 + with: + name: openssl4-${{ matrix.arch }} + path: out_android + retention-days: 30 + package: + needs: build + runs-on: ubuntu-24.04 + steps: + - uses: actions/download-artifact@v8 + with: + pattern: openssl4-* + merge-multiple: true + path: out_android + - name: Package the verified ABI set + run: | + mkdir -p artifacts/bin artifacts/include + for abi in arm64-v8a armeabi-v7a x86_64; do + test -s "out_android/$abi/libnode.so" + grep -a -o -m1 'OpenSSL 4\.0\.3' "out_android/$abi/libnode.so" + cp -R "out_android/$abi" artifacts/bin/ + done + cp -R out_android/libnode/include/* artifacts/include/ + - uses: actions/upload-artifact@v7 + with: + name: nodejs-mobile-android-lite + path: artifacts + retention-days: 90 + smoke: + needs: package + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v7 + - uses: ./.github/actions/materialize + - uses: nttld/setup-ndk@ed92fe6cadad69be94a966a7ee3271275e62f779 + id: ndk + with: + ndk-version: r29 + add-to-path: false + - uses: actions/download-artifact@v8 + with: + name: nodejs-mobile-android-lite + path: artifacts + - name: Enable KVM and build embedder + env: + NDK: ${{ steps.ndk.outputs.ndk-path }} + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + ./tools/mobile-test/smoke/build-android-embedder.sh "$NDK" x86_64 "$PWD/artifacts/bin/x86_64" "$PWD/node_smoke" + cp "$NDK/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/x86_64-linux-android/libc++_shared.so" "$PWD/libc++_shared.so" + - uses: reactivecircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d + with: + api-level: 30 + arch: x86_64 + target: default + force-avd-creation: false + emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + disable-animations: true + script: ./tools/mobile-test/smoke/run-android-smoke.sh "$PWD/node_smoke" "$PWD/artifacts/bin/x86_64/libnode.so" "$PWD/libc++_shared.so" && ./tools/mobile-test/smoke/run-android-quic-smoke.sh "$PWD/node_smoke" "$PWD/artifacts/bin/x86_64/libnode.so" "$PWD/libc++_shared.so" diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 55d0f4e30b..a9ad488680 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -51,9 +51,11 @@ changes only the upstream-workflow removal patch. - Android links a prebuilt static OpenSSL 4.0.3 tree built by `tools/android_openssl.sh` (`--shared-openssl`, SHA-256-verified release - asset) instead of the bundled 3.5.x line — the library prerequisite for - future ECH work. iOS and the bundled-OpenSSL fallback are unchanged; - `node:quic` remains disabled and no ECH API is enabled by this change. + asset) instead of the bundled 3.5.x line. Android enables native QUIC/H3 + with the vendored ngtcp2/nghttp3 libraries. TLS and QUIC client options + accept a mandatory ECHConfigList and expose authenticated acceptance; + rejected ECH cannot fall back to plaintext SNI. iOS and the bundled + OpenSSL fallback retain their previous library and no ECH support. ## 2026-10-07, Version 26.11.0-0 (Current) diff --git a/docs/OPENSSL-4.md b/docs/OPENSSL-4.md index 0feb35d481..43778c1c58 100644 --- a/docs/OPENSSL-4.md +++ b/docs/OPENSSL-4.md @@ -69,8 +69,8 @@ Android boot smokes and both Android curated suites. The full device suites are skipped on manual dispatches, so this is a preview gate, not a release run. Every Android job re-verified the embedded version string, and a downloaded `nodejs-mobile-android-lite` artifact carries `OpenSSL 4.0.3` in -all three ABIs. `node:quic` remains disabled on this line; wiring ECH into -node:tls / node:quic is the follow-up. +all three ABIs. That older run had QUIC disabled and did not contain the +ECH client APIs described above. ## Fall back to the bundled line diff --git a/docs/VERSION-LINES.md b/docs/VERSION-LINES.md index 8832f27ce5..81618539c6 100644 --- a/docs/VERSION-LINES.md +++ b/docs/VERSION-LINES.md @@ -48,8 +48,9 @@ partition and expected source-tree checksum. The Node 26 line uses Android NDK r29 (`29.0.14206865`) and Clang 19 for Linux host tools. Node 24 keeps its existing NDK r27d configuration. Android also links a prebuilt static OpenSSL 4.0.3 tree instead of the -bundled 3.5.x line ([OPENSSL-4.md](OPENSSL-4.md)); `node:quic` stays -disabled and no ECH API is enabled by that change alone. +bundled 3.5.x line. The fork also exposes mandatory `echConfigList` client +options in TLS and QUIC and enables Android native H3; see +[OPENSSL-4.md](OPENSSL-4.md) for the API and validation record. This Node 26.11 preview’s full builds enable V8 Temporal and the experimental `node:ffi` module on Android and iOS, with complete ICU data (`--with-intl=full-icu`) diff --git a/expected-tree.txt b/expected-tree.txt index 1da1c08e8d..0c2ae39a87 100644 --- a/expected-tree.txt +++ b/expected-tree.txt @@ -14,4 +14,4 @@ # run prepare.sh (or regenerate-patches.py, which prints the hash) and # update the value below in the same commit. If you forget, CI fails and # prints the hash it got. -7d3d6efed37ff4a08c95bb0301d6fd8fde0c56e4 +eee39d6ea85fc6c276135b30d50001b3781145f9 diff --git a/mobile-src/test/parallel/test-mobile-ech.js b/mobile-src/test/parallel/test-mobile-ech.js index 592f417964..d1b852f22f 100644 --- a/mobile-src/test/parallel/test-mobile-ech.js +++ b/mobile-src/test/parallel/test-mobile-ech.js @@ -11,7 +11,7 @@ assert.strictEqual(typeof tls.ECH_SUPPORTED, 'boolean'); assert.strictEqual(typeof tls.TLSSocket.prototype.getECHStatus, 'function'); if (process.platform === 'android' && /^4\./.test(process.versions.openssl)) { assert.strictEqual(tls.ECH_SUPPORTED, true); - assert.strictEqual(process.features.quic, true); + assert.strictEqual(String(process.config.variables.node_use_quic), 'true'); } assert.throws(() => tls.connect({ echConfigList: 'invalid' }), { code: 'ERR_INVALID_ARG_TYPE' }); diff --git a/mobile-src/tools/mobile-test/smoke/quic-h3.js b/mobile-src/tools/mobile-test/smoke/quic-h3.js new file mode 100644 index 0000000000..c9d330f58e --- /dev/null +++ b/mobile-src/tools/mobile-test/smoke/quic-h3.js @@ -0,0 +1,57 @@ +'use strict'; + +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const { createPrivateKey } = require('node:crypto'); +const { listen, connect } = require('node:quic'); + +async function main() { + assert.equal(require('node:tls').ECH_SUPPORTED, true); + const key = createPrivateKey(fs.readFileSync(path.join(__dirname, 'key.pem'))); + const cert = fs.readFileSync(path.join(__dirname, 'cert.pem')); + const sessions = []; + let endpoint, client; + try { + endpoint = await listen((session) => { + sessions.push(session); + session.closed.catch(() => {}); + session.onstream = (stream) => stream.closed.catch(() => {}); + }, { + sni: { '*': { keys: [key], certs: [cert] } }, + onheaders(headers) { + assert.equal(headers[':method'], 'GET'); + this.sendHeaders({ ':status': '200', 'content-type': 'text/plain' }); + this.writer.writeSync(Buffer.from('native-h3')); + this.writer.endSync(); + }, + }); + client = await connect(endpoint.address, { + servername: 'localhost', ca: cert, verifyPeer: 'strict', + keepAlive: 0, enableEarlyData: false, + }); + client.closed.catch(() => {}); + assert.equal((await client.opened).protocol, 'h3'); + let resolveHeaders; + const received = new Promise((resolve) => { resolveHeaders = resolve; }); + const stream = await client.createBidirectionalStream({ + headers: { ':method': 'GET', ':scheme': 'https', ':authority': 'localhost', ':path': '/' }, + onheaders(headers) { resolveHeaders(headers); }, + }); + assert.equal((await received)[':status'], 200); + const chunks = []; + for await (const batch of stream) { + for (const chunk of Array.isArray(batch) ? batch : [batch]) chunks.push(Buffer.from(chunk)); + } + assert.equal(Buffer.concat(chunks).toString(), 'native-h3'); + console.log('NODEJS_MOBILE_QUIC_H3_OK'); + } finally { + const clientEndpoint = client?.endpoint; + client?.destroy(); + await clientEndpoint?.close(); + for (const session of sessions) session.destroy(); + await endpoint?.close(); + } +} + +main().catch((error) => { console.error(error); process.exitCode = 1; }); diff --git a/mobile-src/tools/mobile-test/smoke/run-android-quic-smoke.sh b/mobile-src/tools/mobile-test/smoke/run-android-quic-smoke.sh new file mode 100755 index 0000000000..c4dc2bd22c --- /dev/null +++ b/mobile-src/tools/mobile-test/smoke/run-android-quic-smoke.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# Device-only H3 regression: the first server Initial must not use an old clock. +set -euo pipefail +EMBEDDER=$1 +LIBNODE=$2 +LIBCXX=$3 +SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) +DEST=/data/local/tmp/nm-quic-smoke +adb shell "mkdir -p $DEST" +adb push "$EMBEDDER" "$DEST/node_smoke" >/dev/null +adb push "$LIBNODE" "$DEST/libnode.so" >/dev/null +adb push "$LIBCXX" "$DEST/libc++_shared.so" >/dev/null +adb push "$SCRIPT_DIR/quic-h3.js" "$DEST/quic-h3.js" >/dev/null +openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \ + -subj /CN=localhost -addext subjectAltName=DNS:localhost \ + -keyout "$RUNNER_TEMP/quic-key.pem" -out "$RUNNER_TEMP/quic-cert.pem" -days 1 +adb push "$RUNNER_TEMP/quic-key.pem" "$DEST/key.pem" >/dev/null +adb push "$RUNNER_TEMP/quic-cert.pem" "$DEST/cert.pem" >/dev/null +adb shell "chmod 755 $DEST/node_smoke" +OUTPUT=$(adb shell "cd $DEST && LD_LIBRARY_PATH=$DEST TMPDIR=$DEST timeout 30 ./node_smoke --experimental-quic quic-h3.js; echo QUIC_EXIT:\$?") +echo "$OUTPUT" +grep -q NODEJS_MOBILE_QUIC_H3_OK <<< "$OUTPUT" +grep -q QUIC_EXIT:0 <<< "$OUTPUT" diff --git a/patches/files.map b/patches/files.map index a26a812172..0aa366fe05 100644 --- a/patches/files.map +++ b/patches/files.map @@ -148,3 +148,4 @@ src-expose-required-ECH-for-TLS-and-QUIC-clients.patch src/quic/bindingdata.h src-expose-required-ECH-for-TLS-and-QUIC-clients.patch src/quic/session.cc src-expose-required-ECH-for-TLS-and-QUIC-clients.patch src/quic/tlscontext.cc src-expose-required-ECH-for-TLS-and-QUIC-clients.patch src/quic/tlscontext.h +src-fix-QUIC-receive-time-after-session-creation.patch src/quic/endpoint.cc diff --git a/patches/series b/patches/series index 3448b8fe88..5b20bc840a 100644 --- a/patches/series +++ b/patches/series @@ -21,3 +21,4 @@ lib-install-a-WebAssembly-polyfill-when-the-engine-h.patch build-mobile-Temporal-Rust-toolsets-and-libffi-iOS-c.patch build-profile-ICU-data-for-danmu_api-lite-runtimes.patch src-expose-required-ECH-for-TLS-and-QUIC-clients.patch +src-fix-QUIC-receive-time-after-session-creation.patch diff --git a/patches/src-fix-QUIC-receive-time-after-session-creation.patch b/patches/src-fix-QUIC-receive-time-after-session-creation.patch new file mode 100644 index 0000000000..82a6fd1df1 --- /dev/null +++ b/patches/src-fix-QUIC-receive-time-after-session-creation.patch @@ -0,0 +1,39 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: fogtape +Date: Thu, 15 Oct 2026 10:40:00 +0800 +Subject: [PATCH] src: fix QUIC receive time after session creation + +A server Initial packet creates an ngtcp2 connection after Endpoint::Receive +caches its clock. Passing that earlier timestamp to the first read violates +ngtcp2 monotonicity and aborts the process. Resolve read time inside +Session::ReadPacket, after connection creation, while retaining receive +batching and deferred writes. + +Assisted-by: Codex +--- + src/quic/endpoint.cc | 8 +++++--- + 1 file changed, 5 insertions(+), 3 deletions(-) + +diff --git a/src/quic/endpoint.cc b/src/quic/endpoint.cc +index 13ca955fa2..e12efd5f51 100644 +--- a/src/quic/endpoint.cc ++++ b/src/quic/endpoint.cc +@@ -1392,13 +1392,15 @@ void Endpoint::Receive(const uint8_t* data, + // received in the same I/O burst are processed before any responses + // are generated. The deferred flush via BindingData's uv_check + // callback calls SendPendingData once per dirty session after all +- // packets in the burst have been read. ++ // packets in the burst have been read. Let ReadPacket sample its own ++ // timestamp: accepting an Initial packet constructs a session after the ++ // endpoint's cached `now`, and ngtcp2 requires read timestamps to be at ++ // least settings.initial_ts. Reusing `now` aborts the first server read. + if (session->ReadPacket(pkt_data, + pkt_len, + local_address, + remote_address, +- PacketInfo(), +- now)) { ++ PacketInfo())) { + STAT_INCREMENT_N(Stats, bytes_received, pkt_len); + STAT_INCREMENT(Stats, packets_received); + } From bf1593eb66c4ef0897b136ed18abd5368ab77e76 Mon Sep 17 00:00:00 2001 From: fogtape Date: Thu, 15 Oct 2026 10:58:30 +0800 Subject: [PATCH 11/16] ci: allow device tests to reuse a selected Android lite build --- .github/workflows/android-emulator-tests.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/android-emulator-tests.yml b/.github/workflows/android-emulator-tests.yml index 30b9eff71c..0467a0ed7a 100644 --- a/.github/workflows/android-emulator-tests.yml +++ b/.github/workflows/android-emulator-tests.yml @@ -23,6 +23,16 @@ on: required: false default: "" workflow_dispatch: + inputs: + build_run_id: + description: Build run containing the combined Android artifacts + type: string + required: true + flavor: + description: Runtime flavors available in that build + type: choice + options: [both, full, lite] + default: both env: ANDROID_NDK_VERSION: r29 @@ -51,7 +61,7 @@ jobs: strategy: fail-fast: false matrix: - flavor: [full, lite] + flavor: ${{ fromJSON(inputs.flavor == 'lite' && '["lite"]' || inputs.flavor == 'full' && '["full"]' || '["full","lite"]') }} runs-on: ubuntu-24.04 timeout-minutes: 90 From a8a46ef5c94228a5a247d8aa1ba59a7212eed061 Mon Sep 17 00:00:00 2001 From: fogtape Date: Thu, 15 Oct 2026 11:04:05 +0800 Subject: [PATCH 12/16] build: align Android LOAD and RELRO to 16 KB and verify artifacts --- .github/workflows/build.yml | 2 +- .github/workflows/openssl4-android.yml | 2 +- docs/OPENSSL-4.md | 5 +++ expected-tree.txt | 2 +- mobile-src/tools/android_build.sh | 5 +++ mobile-src/tools/check-android-elf.py | 44 ++++++++++++++++++++++++++ 6 files changed, 57 insertions(+), 3 deletions(-) create mode 100644 mobile-src/tools/check-android-elf.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1040d6caa1..5e82164aba 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -135,7 +135,7 @@ jobs: # change to these flags must invalidate the cached libnode. Never in # workflow env: it would leak into the iOS link, which Apple's ld # rejects ("ld: unknown options: -z"). - ANDROID_LDFLAGS: "-Wl,-z,max-page-size=16384" + ANDROID_LDFLAGS: "-Wl,-z,max-page-size=16384 -Wl,-z,common-page-size=16384" steps: - uses: actions/checkout@v7 diff --git a/.github/workflows/openssl4-android.yml b/.github/workflows/openssl4-android.yml index 802166f0bb..eedf847b1e 100644 --- a/.github/workflows/openssl4-android.yml +++ b/.github/workflows/openssl4-android.yml @@ -26,7 +26,7 @@ jobs: matrix: arch: [arm64, arm, x86_64] env: - LDFLAGS: -Wl,-z,max-page-size=16384 + LDFLAGS: -Wl,-z,max-page-size=16384 -Wl,-z,common-page-size=16384 steps: - uses: actions/checkout@v7 - uses: ./.github/actions/materialize diff --git a/docs/OPENSSL-4.md b/docs/OPENSSL-4.md index 43778c1c58..aaec411f85 100644 --- a/docs/OPENSSL-4.md +++ b/docs/OPENSSL-4.md @@ -59,6 +59,11 @@ that build predates these APIs and does not validate ECH/H3. `grep -a -o -m1 'OpenSSL 4\.0\.[0-9]*' out_android//libnode.so`. The Build workflow repeats this after restore, so cache hits are covered. - On device, `process.versions.openssl` reports `4.0.3`. +- Android builds set both `max-page-size=16384` and `common-page-size=16384`. + `tools/check-android-elf.py` rejects a library unless every LOAD segment + has 16 KB alignment and congruent file/virtual offsets, and every GNU_RELRO + segment ends at a 16 KB boundary. Checking only LOAD alignment misses + libraries whose RELRO padding still uses 4 KB pages. ## Validation record diff --git a/expected-tree.txt b/expected-tree.txt index 0c2ae39a87..928ee965ba 100644 --- a/expected-tree.txt +++ b/expected-tree.txt @@ -14,4 +14,4 @@ # run prepare.sh (or regenerate-patches.py, which prints the hash) and # update the value below in the same commit. If you forget, CI fails and # prints the hash it got. -eee39d6ea85fc6c276135b30d50001b3781145f9 +7b3803dc0f15a07cf49786ebcba308d37a5ed435 diff --git a/mobile-src/tools/android_build.sh b/mobile-src/tools/android_build.sh index b2f030c928..cce10df816 100755 --- a/mobile-src/tools/android_build.sh +++ b/mobile-src/tools/android_build.sh @@ -22,6 +22,10 @@ ANDROID_NDK_PATH=${PWD} cd "$SCRIPT_DIR" cd ../ +# Keep RELRO padding and load segments compatible with 16 KB devices, including +# local builds that do not inherit the CI job's linker flags. +export LDFLAGS="${LDFLAGS:-} -Wl,-z,max-page-size=16384 -Wl,-z,common-page-size=16384" + BUILD_ARCH() { # Clean previous compilation make clean @@ -69,6 +73,7 @@ BUILD_ARCH() { python3 tools/strip-android-runtime.py --ndk "$ANDROID_NDK_PATH" \ "out_android/$TARGET_ARCH_FOLDER/libnode.so" fi + python3 tools/check-android-elf.py "out_android/$TARGET_ARCH_FOLDER/libnode.so" } if [ $# -eq 2 ]; then diff --git a/mobile-src/tools/check-android-elf.py b/mobile-src/tools/check-android-elf.py new file mode 100644 index 0000000000..d0f35469bd --- /dev/null +++ b/mobile-src/tools/check-android-elf.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +"""Check Android shared libraries' LOAD and RELRO alignment using ELF headers.""" +import pathlib +import struct +import sys + + +def check(path): + data = path.read_bytes() + if data[:4] != b'\x7fELF' or data[4] not in (1, 2) or data[5] != 1: + raise ValueError('expected a little-endian ELF32/ELF64 library') + is64 = data[4] == 2 + phoff = struct.unpack_from(' len(data): + raise ValueError('invalid program header table') + loads = 0 + for index in range(count): + fields = struct.unpack_from(' Date: Sun, 18 Oct 2026 13:36:27 +0800 Subject: [PATCH 13/16] upgrade: rebase the patch series onto Node v26.12.0 Pin the official v26.12.0 tag (26e72d85a611e222e3f06025de8a3c496923bacd) and bump the mobile version to 26.12.0-0. All 24 patches apply: 15 are byte-identical and 9 changed only in context/line numbers or in the upstream-workflow removal contents. The OpenSSL 4 ECH/H3 and QUIC patches preserve their behavior. A clean reconstruction reproduces tree 48d300ec15fe3f152d51299f430dca7752ca98fb (expected-tree.txt). Local recipe tests pass, including 4146 Android suite cases across 16 shards. Android/iOS compilation and device validation remain for CI; danmu-api-android stays on its current runtime until that succeeds. Leave release-ready.txt and release-upstream-snapshot.txt empty: this is a source upgrade, not an armed release. Assisted-by: Codex --- .github/workflows/openssl4-android.yml | 2 +- README.md | 16 ++-- docs/BUILDING.md | 6 +- docs/CHANGELOG.md | 20 +++++ docs/DANMU-LITE.md | 2 +- docs/UPGRADING.md | 3 +- docs/UPSTREAM-26.11.md | 34 +++++--- docs/VERSION-LINES.md | 10 +-- expected-tree.txt | 2 +- mobile-src/src/node_mobile_version.h | 4 +- ...S-dest-os-and-toolchain-configure.py.patch | 6 +- ...ile-platform-settings-in-common.gypi.patch | 2 +- ...mobile-targets-in-node.gyp-node.gypi.patch | 12 +-- ...e-upstream-only-workflows-and-config.patch | 80 ++++++++++--------- ...bAssembly-polyfill-when-the-engine-h.patch | 6 +- ...bile-adjustment-in-crypto_context.cc.patch | 10 +-- ...equired-ECH-for-TLS-and-QUIC-clients.patch | 18 ++--- ...orm-guards-in-node.cc-node_internals.patch | 4 +- ...parallel-and-sequential-tests-for-mo.patch | 2 +- release-ready.txt | 1 - release-upstream-snapshot.txt | 1 - upstream-base.txt | 7 +- 22 files changed, 138 insertions(+), 110 deletions(-) diff --git a/.github/workflows/openssl4-android.yml b/.github/workflows/openssl4-android.yml index eedf847b1e..527fcfb7d4 100644 --- a/.github/workflows/openssl4-android.yml +++ b/.github/workflows/openssl4-android.yml @@ -2,7 +2,7 @@ name: OpenSSL 4 Android client on: push: - branches: [openssl4/node26.11.1] + branches: [openssl4/node26.12.0] paths: [patches/**, mobile-src/**, expected-tree.txt, .github/workflows/openssl4-android.yml] workflow_dispatch: diff --git a/README.md b/README.md index 70011add2d..536ca70e87 100644 --- a/README.md +++ b/README.md @@ -9,19 +9,19 @@ in-app, with npm modules and N-API native addons. | Android | `libnode.so` + headers | `arm64-v8a`, `armeabi-v7a`, `x86_64` (16 KB page size) | | iOS | `NodeMobile.xcframework` | `arm64` device, `arm64` simulator | -Current line: **Node.js 26** (`v26.11.1`, official release). -This checkout pins the official tag `v26.11.1` at commit -`364593d0631def13e3043dd84f77ebca2c767ce5`; it rebases the reviewed -[proposal #66546](https://github.com/nodejs/node/pull/66546) snapshot to -the patch release upstream published afterwards. [Provenance and -validation](docs/UPSTREAM-26.11.md) record the rebase. +Current line: **Node.js 26** (`v26.12.0`, official release). +This checkout pins the official tag `v26.12.0` at commit +`26e72d85a611e222e3f06025de8a3c496923bacd`. The patch series moves forward +from the reviewed 26.11 line with no mobile-code changes beyond upstream +context updates. [Provenance and validation](docs/UPSTREAM-26.11.md) record +the rebase. Android builds link a prebuilt static OpenSSL 4.0.3 tree instead of the bundled 3.5.x line; see [Android OpenSSL 4.x](docs/OPENSSL-4.md). The stable Node 24 line continues independently on [`recipe`](https://github.com/fogtape/nodejs-mobile/tree/recipe). See [maintained version lines](docs/VERSION-LINES.md) for branch selection, build instructions and download locations. `process.version` reports the upstream version unchanged, so version-parsing tools keep working; the -mobile build is identified by `process.versions.mobile` (e.g. `"26.11.1-0"`). +mobile build is identified by `process.versions.mobile` (e.g. `"26.12.0-0"`). ## Get it @@ -60,7 +60,7 @@ for building one — about 1.5 MB instead of a 1 GB fork: | | | |---|---| -| `upstream-base.txt` | the pinned upstream PR commit (see [26.11 provenance](docs/UPSTREAM-26.11.md)) | +| `upstream-base.txt` | the pinned upstream release tag (see [26.11 provenance](docs/UPSTREAM-26.11.md)) | | `patches/` | per-concern patches to upstream files, plus `series` (apply order) and `files.map` (which patch owns which file) | | `mobile-src/` | files that have no upstream counterpart — build scripts, the iOS framework project, test apps and harness | | `expected-tree.txt` | the git tree hash the reconstruction must produce | diff --git a/docs/BUILDING.md b/docs/BUILDING.md index a8464e3e83..8a58cf50aa 100644 --- a/docs/BUILDING.md +++ b/docs/BUILDING.md @@ -1,8 +1,8 @@ # Build Instructions These instructions describe the Node 26 line. This checkout is the pinned -26.11 upstream PR preview on `upgrade/node26.11-mobile-lite`; -see [provenance](UPSTREAM-26.11.md). For Node 24, +26.12 upstream line on `openssl4/node26.12.0`; see +[provenance](UPSTREAM-26.11.md). For Node 24, use the `recipe` branch and its toolchain instructions. See [VERSION-LINES.md](VERSION-LINES.md) for the maintenance policy and the Node 26 FFI/Temporal configuration. @@ -88,7 +88,7 @@ sdkmanager "ndk;29.0.14206865" This repository holds the recipe, not the source. Generate a tree from it: ```sh -git clone -b upgrade/node26.11-mobile-lite https://github.com/fogtape/nodejs-mobile +git clone -b openssl4/node26.12.0 https://github.com/fogtape/nodejs-mobile cd nodejs-mobile && scripts/prepare.sh && cd out ``` diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index a9ad488680..47376f84e3 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -6,6 +6,7 @@ +26.12.0-0
26.11.1-0
26.11.0-0
26.10.0-0
@@ -43,6 +44,25 @@ + +## 2026-10-18, Version 26.12.0-0 (Current) + +- Rebase the mobile patch series onto the official Node.js `v26.12.0` tag + (`26e72d85a611e222e3f06025de8a3c496923bacd`). All 24 patches apply; the + OpenSSL 4 ECH/H3 and QUIC changes carry forward unchanged, and the + upstream-workflow removal patch absorbs 16 upstream workflow edits. +- 26.12.0 is a regular Current minor release with no security designation: + V8, libuv, npm and OpenSSL versions are unchanged from 26.11.1. Relevant + mobile changes are upstream HTTP/stream/ESM fixes and the new + `http.websocketMask()` / `websocketUnmask()` helpers. +- Keep the danmu lite feature contract unchanged: complete ICU API with the + reviewed data profile, web encodings, Chinese collation and normalization; + omit Temporal, FFI, Inspector, SQLite/Web Storage, Amaro and SEA. Android + retains JIT/native WASM, JNI/ECH/H3 and 16 KB page alignment. +- This source upgrade does not itself switch `danmu-api-android` to 26.12. + A new runtime archive, checksum update and device validation are separate, + reviewed steps. + ## 2026-10-14, Version 26.11.1-0 (Current) diff --git a/docs/DANMU-LITE.md b/docs/DANMU-LITE.md index 40fcef80f5..9803e9a0de 100644 --- a/docs/DANMU-LITE.md +++ b/docs/DANMU-LITE.md @@ -29,7 +29,7 @@ cannot be inferred from today's imports. before/after trimming and lets icupkg validate package dependencies. A unique intermediate package name prevents an installed host's full ICU from hiding missing resources. The final conventional data entry point is preserved. -With the Node 26.11 proposal’s bundled ICU data, the production helper +With the Node 26.12 line’s bundled ICU data, the production helper reduces 33,107,952 to 11,026,480 bytes (22,081,472 bytes removed). This measures data only, not a complete newly compiled libnode or compressed ZIP. diff --git a/docs/UPGRADING.md b/docs/UPGRADING.md index ca62a20868..219d312e63 100644 --- a/docs/UPGRADING.md +++ b/docs/UPGRADING.md @@ -23,7 +23,8 @@ PR snapshots can run normal builds and produce CI preview packages. Cut release and prerelease publication reject a SHA baseline. After upstream releases, compare the proposal commit with the official tag, rebase/regenerate as needed, update the tree checksum, and complete the build/device gates before release -preparation. The [26.11 preview](UPSTREAM-26.11.md) records the current pin. +preparation. The [26.11 provenance](UPSTREAM-26.11.md) records the current +pin and the earlier proposal snapshot. ## Re-base the series diff --git a/docs/UPSTREAM-26.11.md b/docs/UPSTREAM-26.11.md index 37708bb202..9a9bdcf619 100644 --- a/docs/UPSTREAM-26.11.md +++ b/docs/UPSTREAM-26.11.md @@ -1,17 +1,25 @@ -# Node 26.11 upstream line - -`upstream-base.txt` pins the official tag `v26.11.1` -(`364593d0631def13e3043dd84f77ebca2c767ce5`), the patch release that -followed the reviewed 26.11.0 proposal snapshot described below. The rebase -re-emitted 21 of the 22 patches byte-identically; the workflow removal -patch absorbed upstream's edits to `.github/workflows/build-tarball.yml` -(`modify/delete`, resolved as deletion per that patch's intent). The mobile -version is `26.11.1-0`, and `expected-tree.txt` records the new -source-tree checksum. - -On top of this rebase, Android links a prebuilt OpenSSL 4.0.3 static tree +# Node 26 upstream line + +`upstream-base.txt` pins the official tag `v26.12.0` +(`26e72d85a611e222e3f06025de8a3c496923bacd`), which follows the reviewed +26.11.1 line described below. The rebase re-emitted the same 24 patches; +only context/line-number changes and the workflow-removal patch's updated +upstream file contents differ. The mobile version is `26.12.0-0`, and +`expected-tree.txt` records the new source-tree checksum. + +On top of this line, Android links a prebuilt OpenSSL 4.0.3 static tree ([OPENSSL-4.md](OPENSSL-4.md)); iOS and the bundled-OpenSSL fallback are -unchanged. Build run 38581872268 validated the combination (details in +unchanged. The OpenSSL 4 ECH/H3 APIs and QUIC patches apply unchanged to +26.12.0; rebuild and device validation remain required before switching an +app to this runtime. + +## Previous 26.11 line + +The 26.11.1 base was `364593d0631def13e3043dd84f77ebca2c767ce5`. The +rebase from the 26.11.0 proposal snapshot re-emitted 21 of 22 patches +byte-identically; the workflow removal patch absorbed upstream's +`modify/delete` edits and kept the deletion. Build run 38581872268 +validated the OpenSSL 4 combination (details in [OPENSSL-4.md](OPENSSL-4.md)). This recipe previews official Node.js release proposal diff --git a/docs/VERSION-LINES.md b/docs/VERSION-LINES.md index 81618539c6..bf28b9c287 100644 --- a/docs/VERSION-LINES.md +++ b/docs/VERSION-LINES.md @@ -7,10 +7,10 @@ versioned together on that branch. | Branch | Upstream base | Purpose | Outputs | | --- | --- | --- | --- | | `recipe` | Node.js 24.21.0 | Stable Node 24 maintenance and releases | Android and iOS, full and lite | -| `recipe-v26` | Node.js 26.11.1 (official tag) | Independent Node 26 maintenance and releases | Android and iOS, full and lite | +| `recipe-v26` | Node.js 26.12.0 (official tag) | Independent Node 26 maintenance and releases | Android and iOS, full and lite | -For Node 26.11, clone `recipe-v26`; the line now pins the official -`v26.11.1` tag. The earlier 26.11.0 proposal snapshot — and the explicit +For Node 26, clone `recipe-v26`; the line now pins the official +`v26.12.0` tag. The earlier 26.11.0 proposal snapshot — and the explicit full-SHA input its publication required — stays documented in [26.11 provenance](UPSTREAM-26.11.md). Default releases require an official upstream tag and a reviewed release marker. @@ -52,7 +52,7 @@ bundled 3.5.x line. The fork also exposes mandatory `echConfigList` client options in TLS and QUIC and enables Android native H3; see [OPENSSL-4.md](OPENSSL-4.md) for the API and validation record. -This Node 26.11 preview’s full builds enable V8 Temporal and the experimental `node:ffi` +This Node 26 line’s full builds enable V8 Temporal and the experimental `node:ffi` module on Android and iOS, with complete ICU data (`--with-intl=full-icu`) for named time zones. small-ICU cannot load Temporal timezone information in this upstream version. The build requires Rust **1.86.0** and its standard @@ -99,7 +99,7 @@ run URL with the downloaded files; do not mix platform slices or flavors from different runs. Versioned preview packages additionally contain `BUILD-INFO.json` and are accompanied by `SHA256SUMS`. -The line reports `process.version` as `v26.11.1`; `process.versions.mobile` identifies +The line reports `process.version` as `v26.12.0`; `process.versions.mobile` identifies the mobile build revision. A CI preview is not a published release. **Cut release** also supports `recipe-v26`; its reviewed PR arms a specific version; after merge, manually run **Build** with `operation=prerelease` and that exact mobile version. Push/PR builds do diff --git a/expected-tree.txt b/expected-tree.txt index 928ee965ba..618814380a 100644 --- a/expected-tree.txt +++ b/expected-tree.txt @@ -14,4 +14,4 @@ # run prepare.sh (or regenerate-patches.py, which prints the hash) and # update the value below in the same commit. If you forget, CI fails and # prints the hash it got. -7b3803dc0f15a07cf49786ebcba308d37a5ed435 +48d300ec15fe3f152d51299f430dca7752ca98fb diff --git a/mobile-src/src/node_mobile_version.h b/mobile-src/src/node_mobile_version.h index b45ef547bb..587182fe3d 100644 --- a/mobile-src/src/node_mobile_version.h +++ b/mobile-src/src/node_mobile_version.h @@ -4,8 +4,8 @@ #include "node_version.h" #define NODE_MOBILE_MAJOR_VERSION 26 -#define NODE_MOBILE_MINOR_VERSION 11 -#define NODE_MOBILE_PATCH_VERSION 1 +#define NODE_MOBILE_MINOR_VERSION 12 +#define NODE_MOBILE_PATCH_VERSION 0 // Mobile rebuild revision of the same upstream Node version: the -R in the // vX.Y.Z-R release tag. Computed and bumped by the Cut release workflow diff --git a/patches/build-Android-iOS-dest-os-and-toolchain-configure.py.patch b/patches/build-Android-iOS-dest-os-and-toolchain-configure.py.patch index d32d604cbc..e1760eee6c 100644 --- a/patches/build-Android-iOS-dest-os-and-toolchain-configure.py.patch +++ b/patches/build-Android-iOS-dest-os-and-toolchain-configure.py.patch @@ -166,7 +166,7 @@ index 5cea0393f4..b1a1c6f827 100644 - os.system("./configure --dest-cpu=" + DEST_CPU + " --dest-os=android --openssl-no-asm --cross-compiling") + sys.exit(os.system("./configure --dest-cpu=" + DEST_CPU + " --dest-os=android --with-intl=" + intl + extra_flags + openssl_flags + " --cross-compiling --shared") >> 8) diff --git a/configure.py b/configure.py -index acda6a16c9..5ad9c1e81d 100755 +index a3769ff2d8..c052538d46 100755 --- a/configure.py +++ b/configure.py @@ -1304,6 +1304,11 @@ parser.add_argument('-C', @@ -191,8 +191,8 @@ index acda6a16c9..5ad9c1e81d 100755 if options.dest_os == 'android': o['variables']['OS'] = 'android' o['variables']['node_enable_experimentals'] = b(options.enable_all_experimentals) -@@ -2200,6 +2208,14 @@ def configure_library(lib, output, pkgname=None): - output['variables']['node_' + shared_lib] = b(getattr(options, shared_lib)) +@@ -2201,6 +2209,14 @@ def configure_library(lib, output, pkgname=None, libraries_key='libraries'): + libraries = output[libraries_key] if getattr(options, shared_lib): + # nodejs-mobile: the Android build links the external OpenSSL tree from diff --git a/patches/build-mobile-platform-settings-in-common.gypi.patch b/patches/build-mobile-platform-settings-in-common.gypi.patch index 6e9e0f6483..73edd9b749 100644 --- a/patches/build-mobile-platform-settings-in-common.gypi.patch +++ b/patches/build-mobile-platform-settings-in-common.gypi.patch @@ -23,7 +23,7 @@ Co-authored-by: João Matos 1 file changed, 118 insertions(+), 7 deletions(-) diff --git a/common.gypi b/common.gypi -index de80a68f49..fd3394b5cb 100644 +index 39229ef587..239797d465 100644 --- a/common.gypi +++ b/common.gypi @@ -111,7 +111,7 @@ diff --git a/patches/build-mobile-targets-in-node.gyp-node.gypi.patch b/patches/build-mobile-targets-in-node.gyp-node.gypi.patch index fb19d056e1..04d224db26 100644 --- a/patches/build-mobile-targets-in-node.gyp-node.gypi.patch +++ b/patches/build-mobile-targets-in-node.gyp-node.gypi.patch @@ -27,10 +27,10 @@ Assisted-by: Codex 2 files changed, 63 insertions(+), 11 deletions(-) diff --git a/node.gyp b/node.gyp -index 587df5cace..7fa1892d2d 100644 +index 5334b8a746..ca26defa34 100644 --- a/node.gyp +++ b/node.gyp -@@ -28,6 +28,11 @@ +@@ -29,6 +29,11 @@ 'node_shared_nbytes%': 'false', 'node_shared_nghttp2%': 'false', 'node_shared_openssl%': 'false', @@ -42,7 +42,7 @@ index 587df5cace..7fa1892d2d 100644 'node_shared_perfetto%': 'false', 'node_shared_sqlite%': 'false', 'node_shared_ffi%': 'false', -@@ -77,6 +82,11 @@ +@@ -78,6 +83,11 @@ 'deps/acorn/acorn/dist/acorn.js', 'deps/acorn/acorn-walk/dist/walk.js', 'deps/minimatch/index.js', @@ -54,7 +54,7 @@ index 587df5cace..7fa1892d2d 100644 '<@(node_builtin_shareable_builtins)', ], 'node_sources': [ -@@ -580,6 +590,11 @@ +@@ -581,6 +591,11 @@ ['OS=="linux" and clang==1', { 'libraries': ['-latomic'], }], @@ -66,7 +66,7 @@ index 587df5cace..7fa1892d2d 100644 ], }, -@@ -1427,8 +1442,9 @@ +@@ -1428,8 +1443,9 @@ ['OS=="solaris"', { 'ldflags': [ '-I<(SHARED_INTERMEDIATE_DIR)' ] }], @@ -78,7 +78,7 @@ index 587df5cace..7fa1892d2d 100644 'type': 'none', }], [ 'node_shared=="true"', { -@@ -1652,7 +1668,8 @@ +@@ -1653,7 +1669,8 @@ [ 'node_shared_libuv=="false"', { 'dependencies': [ 'deps/uv/uv.gyp:libuv#host' ], }], diff --git a/patches/ci-remove-upstream-only-workflows-and-config.patch b/patches/ci-remove-upstream-only-workflows-and-config.patch index 2195e89048..630bff5adf 100644 --- a/patches/ci-remove-upstream-only-workflows-and-config.patch +++ b/patches/ci-remove-upstream-only-workflows-and-config.patch @@ -38,7 +38,7 @@ is read only from the default branch and is left untouched. .github/workflows/coverage-linux.yml | 90 ----- .github/workflows/coverage-windows.yml | 106 ----- .github/workflows/create-release-proposal.yml | 81 ---- - .github/workflows/daily-wpt-fyi.yml | 163 -------- + .github/workflows/daily-wpt-fyi.yml | 164 -------- .github/workflows/daily.yml | 30 -- .github/workflows/doc.yml | 41 -- .../workflows/find-inactive-collaborators.yml | 50 --- @@ -60,7 +60,7 @@ is read only from the default branch and is left untouched. .github/workflows/stress-test.yml | 114 ------ .github/workflows/test-internet.yml | 70 ---- .github/workflows/test-linux-perfetto.yml | 73 ---- - .github/workflows/test-linux-quic.yml | 80 ---- + .github/workflows/test-linux-quic.yml | 81 ---- .github/workflows/test-linux.yml | 100 ----- .github/workflows/test-macos.yml | 142 ------- .github/workflows/test-shared.yml | 273 ------------- @@ -69,7 +69,7 @@ is read only from the default branch and is left untouched. .github/workflows/update-openssl.yml | 69 ---- .github/workflows/update-v8.yml | 57 --- .github/workflows/update-wpt.yml | 95 ----- - 49 files changed, 4753 deletions(-) + 49 files changed, 4755 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/config.yml delete mode 100644 .github/SUPPORT.md delete mode 100644 .github/label-pr-config.yml @@ -570,7 +570,7 @@ index 85f3e92f2f..0000000000 - MAX_WORKLOAD: ${{ vars.AUTO_START_CI_MAX_WORKLOAD || '15' }} diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml deleted file mode 100644 -index 0e901de78d..0000000000 +index 9f4ed58447..0000000000 --- a/.github/workflows/benchmark.yml +++ /dev/null @@ -1,271 +0,0 @@ @@ -716,7 +716,7 @@ index 0e901de78d..0000000000 - --arg withPerfetto ${{ matrix.perfetto || false }} \ - --arg ccache '(import {}).sccache' \ - --run ' -- make build-ci -j4 V=1 +- make build-ci -j4 V=1 ${{ contains(format(' {0} ', inputs.category), ' napi ') && '&& make bench-addons-build' || '' }} - ' tools/nix/ci-shell.nix - env: - SCCACHE_LOCAL_RW_MODE: READ_ONLY @@ -946,7 +946,7 @@ index 73f7ec6afb..0000000000 - ' "$TAR_DIR/tools/nix/ci-shell.nix" diff --git a/.github/workflows/build-tarball.yml b/.github/workflows/build-tarball.yml deleted file mode 100644 -index bfc8f92dd0..0000000000 +index b3ac6aad47..0000000000 --- a/.github/workflows/build-tarball.yml +++ /dev/null @@ -1,132 +0,0 @@ @@ -1016,7 +1016,7 @@ index bfc8f92dd0..0000000000 - cancel-in-progress: true - -env: -- PYTHON_VERSION: '3.14' +- PYTHON_VERSION: '3.15' - FLAKY_TESTS: keep_retrying - CLANG_VERSION: '19' - @@ -1657,7 +1657,7 @@ index 4035ea9320..0000000000 - gh pr comment "$NUMBER" --repo "$GITHUB_REPOSITORY" --body-file - diff --git a/.github/workflows/coverage-linux-without-intl.yml b/.github/workflows/coverage-linux-without-intl.yml deleted file mode 100644 -index 9b8f8e2fd8..0000000000 +index 4ab6975305..0000000000 --- a/.github/workflows/coverage-linux-without-intl.yml +++ /dev/null @@ -1,90 +0,0 @@ @@ -1697,7 +1697,7 @@ index 9b8f8e2fd8..0000000000 - cancel-in-progress: true - -env: -- PYTHON_VERSION: '3.14' +- PYTHON_VERSION: '3.15' - FLAKY_TESTS: keep_retrying - CLANG_VERSION: '19' - CC: ${{ (github.base_ref == 'main' || github.ref_name == 'main') && 'sccache' || '' }} clang-19 @@ -1748,12 +1748,12 @@ index 9b8f8e2fd8..0000000000 - - name: Clean tmp - run: rm -rf coverage/tmp && rm -rf out - - name: Upload -- uses: codecov/codecov-action@0b35c9ecc4f0529d0eb674914510c22f85b196b4 # v7.1.0 +- uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1 - with: - directory: ./coverage diff --git a/.github/workflows/coverage-linux.yml b/.github/workflows/coverage-linux.yml deleted file mode 100644 -index 0c7fc292d3..0000000000 +index a642c0fada..0000000000 --- a/.github/workflows/coverage-linux.yml +++ /dev/null @@ -1,90 +0,0 @@ @@ -1793,7 +1793,7 @@ index 0c7fc292d3..0000000000 - cancel-in-progress: true - -env: -- PYTHON_VERSION: '3.14' +- PYTHON_VERSION: '3.15' - FLAKY_TESTS: keep_retrying - CLANG_VERSION: '19' - CC: ${{ (github.base_ref == 'main' || github.ref_name == 'main') && 'sccache' || '' }} clang-19 @@ -1844,12 +1844,12 @@ index 0c7fc292d3..0000000000 - - name: Clean tmp - run: rm -rf coverage/tmp && rm -rf out - - name: Upload -- uses: codecov/codecov-action@0b35c9ecc4f0529d0eb674914510c22f85b196b4 # v7.1.0 +- uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1 - with: - directory: ./coverage diff --git a/.github/workflows/coverage-windows.yml b/.github/workflows/coverage-windows.yml deleted file mode 100644 -index a4db4ccc08..0000000000 +index c2ce5c3401..0000000000 --- a/.github/workflows/coverage-windows.yml +++ /dev/null @@ -1,106 +0,0 @@ @@ -1915,7 +1915,7 @@ index a4db4ccc08..0000000000 - cancel-in-progress: true - -env: -- PYTHON_VERSION: '3.14' +- PYTHON_VERSION: '3.15' - FLAKY_TESTS: keep_retrying - RUSTC_VERSION: '1.86' - @@ -1956,7 +1956,7 @@ index a4db4ccc08..0000000000 - - name: Clean tmp - run: npx rimraf ./coverage/tmp - - name: Upload -- uses: codecov/codecov-action@0b35c9ecc4f0529d0eb674914510c22f85b196b4 # v7.1.0 +- uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1 - with: - directory: ./coverage diff --git a/.github/workflows/create-release-proposal.yml b/.github/workflows/create-release-proposal.yml @@ -2048,10 +2048,10 @@ index 735b15ffd1..0000000000 - BOT_TOKEN: ${{ secrets.GH_USER_TOKEN }} diff --git a/.github/workflows/daily-wpt-fyi.yml b/.github/workflows/daily-wpt-fyi.yml deleted file mode 100644 -index d15f21010c..0000000000 +index b16b640a08..0000000000 --- a/.github/workflows/daily-wpt-fyi.yml +++ /dev/null -@@ -1,163 +0,0 @@ +@@ -1,164 +0,0 @@ -# This workflow runs every night and tests various releases of Node.js -# (latest nightly, current, and two latest LTS release lines) against the -# `epochs/daily` branch of WPT. @@ -2067,6 +2067,7 @@ index d15f21010c..0000000000 - - cron: 30 0 * * * - -env: +- # WPT's pywebsocket3 still uses CGIHTTPRequestHandler, removed in Python 3.15. - PYTHON_VERSION: '3.14' - -permissions: @@ -2483,7 +2484,7 @@ index 35d780ae66..0000000000 - run: gh issue edit "$NUMBER" --repo "$GITHUB_REPOSITORY" --add-label "$LABELS" diff --git a/.github/workflows/label-pr.yml b/.github/workflows/label-pr.yml deleted file mode 100644 -index 03aa1fa9a8..0000000000 +index 3c50701224..0000000000 --- a/.github/workflows/label-pr.yml +++ /dev/null @@ -1,18 +0,0 @@ @@ -2501,7 +2502,7 @@ index 03aa1fa9a8..0000000000 - runs-on: ubuntu-slim - - steps: -- - uses: nodejs/node-pr-labeler@d4cf1b8b9f23189c37917000e5e17e796c770a6b # v1 +- - uses: nodejs/node-pr-labeler@dfe5eb3b099a487644e32567a482744620876ab3 # v1 - with: - repo-token: ${{ secrets.GH_USER_TOKEN }} - configuration-path: .github/label-pr-config.yml @@ -2551,7 +2552,7 @@ index 3bdf3fc31d..0000000000 - labels: meta diff --git a/.github/workflows/lint-release-proposal.yml b/.github/workflows/lint-release-proposal.yml deleted file mode 100644 -index 027aee336a..0000000000 +index 9652eea91b..0000000000 --- a/.github/workflows/lint-release-proposal.yml +++ /dev/null @@ -1,119 +0,0 @@ @@ -2567,7 +2568,7 @@ index 027aee336a..0000000000 - cancel-in-progress: true - -env: -- PYTHON_VERSION: '3.14' +- PYTHON_VERSION: '3.15' - NODE_VERSION: lts/* - -permissions: @@ -2676,7 +2677,7 @@ index 027aee336a..0000000000 - GH_TOKEN: ${{ github.token }} diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml deleted file mode 100644 -index 9afdb2ef5a..0000000000 +index a4997b3d4c..0000000000 --- a/.github/workflows/linters.yml +++ /dev/null @@ -1,278 +0,0 @@ @@ -2696,7 +2697,7 @@ index 9afdb2ef5a..0000000000 - cancel-in-progress: true - -env: -- PYTHON_VERSION: '3.14' +- PYTHON_VERSION: '3.15' - NODE_VERSION: lts/* - -permissions: @@ -3585,7 +3586,7 @@ index eb5921edb9..0000000000 - remove-stale-when-updated: true diff --git a/.github/workflows/stress-test.yml b/.github/workflows/stress-test.yml deleted file mode 100644 -index 17393eb571..0000000000 +index e47c114d2f..0000000000 --- a/.github/workflows/stress-test.yml +++ /dev/null @@ -1,114 +0,0 @@ @@ -3624,7 +3625,7 @@ index 17393eb571..0000000000 - type: string - -env: -- PYTHON_VERSION: '3.14' +- PYTHON_VERSION: '3.15' - XCODE_VERSION: '16.4' - CLANG_VERSION: '19' - RUSTC_VERSION: '1.86' @@ -3705,7 +3706,7 @@ index 17393eb571..0000000000 - "$TEST_PATH" diff --git a/.github/workflows/test-internet.yml b/.github/workflows/test-internet.yml deleted file mode 100644 -index 43d71f5f31..0000000000 +index 09f52ed2aa..0000000000 --- a/.github/workflows/test-internet.yml +++ /dev/null @@ -1,70 +0,0 @@ @@ -3742,7 +3743,7 @@ index 43d71f5f31..0000000000 - cancel-in-progress: true - -env: -- PYTHON_VERSION: '3.14' +- PYTHON_VERSION: '3.15' - FLAKY_TESTS: keep_retrying - CLANG_VERSION: '19' - CC: ${{ (github.base_ref == 'main' || github.ref_name == 'main') && 'sccache' || '' }} clang-19 @@ -3781,7 +3782,7 @@ index 43d71f5f31..0000000000 - run: make test-internet -j4 V=1; diff --git a/.github/workflows/test-linux-perfetto.yml b/.github/workflows/test-linux-perfetto.yml deleted file mode 100644 -index d2bfbfbd68..0000000000 +index befb170070..0000000000 --- a/.github/workflows/test-linux-perfetto.yml +++ /dev/null @@ -1,73 +0,0 @@ @@ -3807,7 +3808,7 @@ index d2bfbfbd68..0000000000 - cancel-in-progress: true - -env: -- PYTHON_VERSION: '3.14' +- PYTHON_VERSION: '3.15' - FLAKY_TESTS: keep_retrying - CLANG_VERSION: '19' - CC: ${{ (github.base_ref == 'main' || github.ref_name == 'main') && 'sccache' || '' }} clang-19 @@ -3860,10 +3861,10 @@ index d2bfbfbd68..0000000000 - run: git add -A && git diff --name-only --exit-code --staged diff --git a/.github/workflows/test-linux-quic.yml b/.github/workflows/test-linux-quic.yml deleted file mode 100644 -index 0eded996fe..0000000000 +index f9ccef8597..0000000000 --- a/.github/workflows/test-linux-quic.yml +++ /dev/null -@@ -1,80 +0,0 @@ +@@ -1,81 +0,0 @@ -name: Test Linux (with QUIC) - -on: @@ -3874,6 +3875,7 @@ index 0eded996fe..0000000000 - - configure.py - - node.gyp - - node.gypi +- - deps/v8/** - - deps/ngtcp2/** - - deps/nghttp3/** - - deps/openssl/** @@ -3896,7 +3898,7 @@ index 0eded996fe..0000000000 - cancel-in-progress: true - -env: -- PYTHON_VERSION: '3.14' +- PYTHON_VERSION: '3.15' - FLAKY_TESTS: keep_retrying - CLANG_VERSION: '19' - CC: ${{ (github.base_ref == 'main' || github.ref_name == 'main') && 'sccache' || '' }} clang-19 @@ -3946,7 +3948,7 @@ index 0eded996fe..0000000000 - run: git add -A && git diff --name-only --exit-code --staged diff --git a/.github/workflows/test-linux.yml b/.github/workflows/test-linux.yml deleted file mode 100644 -index 74dc45ae8c..0000000000 +index cf10c65467..0000000000 --- a/.github/workflows/test-linux.yml +++ /dev/null @@ -1,100 +0,0 @@ @@ -3991,7 +3993,7 @@ index 74dc45ae8c..0000000000 - cancel-in-progress: true - -env: -- PYTHON_VERSION: '3.14' +- PYTHON_VERSION: '3.15' - FLAKY_TESTS: keep_retrying - CLANG_VERSION: '19' - CC: ${{ (github.base_ref == 'main' || github.ref_name == 'main') && 'sccache' || '' }} clang-19 @@ -4052,7 +4054,7 @@ index 74dc45ae8c..0000000000 - DIR: dir%20with $unusual"chars?'åß∂ƒ©∆¬…` diff --git a/.github/workflows/test-macos.yml b/.github/workflows/test-macos.yml deleted file mode 100644 -index 6954112115..0000000000 +index 22292f2112..0000000000 --- a/.github/workflows/test-macos.yml +++ /dev/null @@ -1,142 +0,0 @@ @@ -4123,7 +4125,7 @@ index 6954112115..0000000000 - cancel-in-progress: true - -env: -- PYTHON_VERSION: '3.14' +- PYTHON_VERSION: '3.15' - XCODE_VERSION: '16.4' - FLAKY_TESTS: keep_retrying - RUSTC_VERSION: '1.86' @@ -4559,7 +4561,7 @@ index abe4516db4..0000000000 - update-pull-request-title-and-body: true diff --git a/.github/workflows/tools.yml b/.github/workflows/tools.yml deleted file mode 100644 -index 497a5c1230..0000000000 +index 8e4c279475..0000000000 --- a/.github/workflows/tools.yml +++ /dev/null @@ -1,365 +0,0 @@ @@ -4613,7 +4615,7 @@ index 497a5c1230..0000000000 - - zstd - -env: -- PYTHON_VERSION: '3.14' +- PYTHON_VERSION: '3.15' - -permissions: - contents: read diff --git a/patches/lib-install-a-WebAssembly-polyfill-when-the-engine-h.patch b/patches/lib-install-a-WebAssembly-polyfill-when-the-engine-h.patch index 1eaa3de85e..ed8a8ccbc2 100644 --- a/patches/lib-install-a-WebAssembly-polyfill-when-the-engine-h.patch +++ b/patches/lib-install-a-WebAssembly-polyfill-when-the-engine-h.patch @@ -15,7 +15,7 @@ jitless/frozen-intrinsics host gates in smoke-host. 2 files changed, 113 insertions(+) diff --git a/LICENSE b/LICENSE -index 67cd2feb69..994885b996 100644 +index fc7b8c7d21..53440510b5 100644 --- a/LICENSE +++ b/LICENSE @@ -2921,3 +2921,26 @@ The externally maintained libraries used by Node.js are: @@ -46,7 +46,7 @@ index 67cd2feb69..994885b996 100644 + SOFTWARE. + """ diff --git a/lib/internal/process/pre_execution.js b/lib/internal/process/pre_execution.js -index 2111579151..9a1b965e5c 100644 +index 96c6a5c757..626dec351b 100644 --- a/lib/internal/process/pre_execution.js +++ b/lib/internal/process/pre_execution.js @@ -10,6 +10,7 @@ const { @@ -65,7 +65,7 @@ index 2111579151..9a1b965e5c 100644 setupTraceCategoryState(); setupInspectorHooks(); setupNetworkInspection(); -@@ -451,6 +453,94 @@ function setupEventsource() { +@@ -449,6 +451,94 @@ function setupEventsource() { } } diff --git a/patches/src-crypto-mobile-adjustment-in-crypto_context.cc.patch b/patches/src-crypto-mobile-adjustment-in-crypto_context.cc.patch index 5118d3f36b..ab67e2f8c7 100644 --- a/patches/src-crypto-mobile-adjustment-in-crypto_context.cc.patch +++ b/patches/src-crypto-mobile-adjustment-in-crypto_context.cc.patch @@ -19,10 +19,10 @@ Co-authored-by: Julia Samól 1 file changed, 17 insertions(+) diff --git a/src/crypto/crypto_context.cc b/src/crypto/crypto_context.cc -index 823d87e7f2..5559b421ce 100644 +index d314d15cc5..ce6b92561e 100644 --- a/src/crypto/crypto_context.cc +++ b/src/crypto/crypto_context.cc -@@ -328,6 +328,9 @@ bool isSelfIssued(X509* cert) { +@@ -318,6 +318,9 @@ bool isSelfIssued(X509* cert) { #ifdef __APPLE__ TrustStatus IsTrustDictionaryTrustedForPolicy(CFDictionaryRef trust_dict, bool is_self_issued) { @@ -32,7 +32,7 @@ index 823d87e7f2..5559b421ce 100644 // Trust settings may be scoped to a single application // skip as this is not supported if (CFDictionaryContainsKey(trust_dict, kSecTrustSettingsApplication)) { -@@ -419,6 +422,10 @@ TrustStatus IsTrustDictionaryTrustedForPolicy(CFDictionaryRef trust_dict, +@@ -409,6 +412,10 @@ TrustStatus IsTrustDictionaryTrustedForPolicy(CFDictionaryRef trust_dict, return (trust_settings_result == kSecTrustSettingsResultTrustAsRoot) ? TrustStatus::TRUSTED : TrustStatus::UNSPECIFIED; @@ -43,7 +43,7 @@ index 823d87e7f2..5559b421ce 100644 } TrustStatus IsTrustSettingsTrustedForPolicy(CFArrayRef trust_settings, -@@ -490,6 +497,8 @@ bool IsCertificateTrustedForPolicy(X509* cert, SecCertificateRef ref) { +@@ -480,6 +487,8 @@ bool IsCertificateTrustedForPolicy(X509* cert, SecCertificateRef ref) { bool trust_evaluated = false; bool is_self_issued = isSelfIssued(cert); @@ -52,7 +52,7 @@ index 823d87e7f2..5559b421ce 100644 // Evaluate user trust domain, then admin. User settings can override // admin (and both override the system domain, but we don't check that). for (const auto& trust_domain : -@@ -528,6 +537,14 @@ bool IsCertificateTrustedForPolicy(X509* cert, SecCertificateRef ref) { +@@ -518,6 +527,14 @@ bool IsCertificateTrustedForPolicy(X509* cert, SecCertificateRef ref) { CFRelease(trust_settings); } } diff --git a/patches/src-expose-required-ECH-for-TLS-and-QUIC-clients.patch b/patches/src-expose-required-ECH-for-TLS-and-QUIC-clients.patch index 057acdd589..d8d94e0d65 100644 --- a/patches/src-expose-required-ECH-for-TLS-and-QUIC-clients.patch +++ b/patches/src-expose-required-ECH-for-TLS-and-QUIC-clients.patch @@ -35,10 +35,10 @@ index 7ae6279248..3dcd7278a1 100644 '<@(ngtcp2_sources_ossl)', ], diff --git a/lib/internal/quic/quic.js b/lib/internal/quic/quic.js -index 4922ce5627..9ae28a6f48 100644 +index d163311dc1..e3e5d0f1fc 100644 --- a/lib/internal/quic/quic.js +++ b/lib/internal/quic/quic.js -@@ -822,13 +822,13 @@ setCallbacks({ +@@ -823,13 +823,13 @@ setCallbacks({ validationErrorReason, validationErrorCode, earlyDataAttempted, @@ -54,7 +54,7 @@ index 4922ce5627..9ae28a6f48 100644 }, /** -@@ -4209,7 +4209,7 @@ class QuicSession { +@@ -4217,7 +4217,7 @@ class QuicSession { * @param {number} validationErrorCode */ [kHandshake](servername, protocol, cipher, cipherVersion, validationErrorReason, @@ -63,7 +63,7 @@ index 4922ce5627..9ae28a6f48 100644 const inner = this.#inner; if (this.destroyed || !inner.pendingOpen.resolve) return; -@@ -4228,6 +4228,7 @@ class QuicSession { +@@ -4236,6 +4236,7 @@ class QuicSession { cipherVersion, validationErrorReason, validationErrorCode, @@ -71,7 +71,7 @@ index 4922ce5627..9ae28a6f48 100644 earlyDataAttempted, earlyDataAccepted, }; -@@ -5231,6 +5232,7 @@ function processIdentityOptions(identity, label) { +@@ -5239,6 +5240,7 @@ function processIdentityOptions(identity, label) { function processTlsOptions(tls, forServer) { const { servername, @@ -79,7 +79,7 @@ index 4922ce5627..9ae28a6f48 100644 alpn, ciphers = DEFAULT_CIPHERS, groups = DEFAULT_GROUPS, -@@ -5252,6 +5254,12 @@ function processTlsOptions(tls, forServer) { +@@ -5260,6 +5262,12 @@ function processTlsOptions(tls, forServer) { if (servername !== undefined) { validateString(servername, 'options.servername'); } @@ -92,7 +92,7 @@ index 4922ce5627..9ae28a6f48 100644 if (ciphers !== undefined) { validateString(ciphers, 'options.ciphers'); } -@@ -5364,6 +5372,7 @@ function processTlsOptions(tls, forServer) { +@@ -5372,6 +5380,7 @@ function processTlsOptions(tls, forServer) { const shared = { __proto__: null, servername, @@ -255,10 +255,10 @@ index ab60240188..23b80c2154 100644 static void SetVerifyMode(const v8::FunctionCallbackInfo& args); static void Start(const v8::FunctionCallbackInfo& args); diff --git a/src/quic/bindingdata.h b/src/quic/bindingdata.h -index 2ef9f76853..037bb56e8a 100644 +index 7d424b9bf6..21e55af1f0 100644 --- a/src/quic/bindingdata.h +++ b/src/quic/bindingdata.h -@@ -90,6 +90,7 @@ class SessionManager; +@@ -91,6 +91,7 @@ struct QuicAllocState; V(disable_stateless_reset, "disableStatelessReset") \ V(draining_period_multiplier, "drainingPeriodMultiplier") \ V(enable_connect_protocol, "enableConnectProtocol") \ diff --git a/patches/src-mobile-platform-guards-in-node.cc-node_internals.patch b/patches/src-mobile-platform-guards-in-node.cc-node_internals.patch index 5bc362d085..cc97c0aeef 100644 --- a/patches/src-mobile-platform-guards-in-node.cc-node_internals.patch +++ b/patches/src-mobile-platform-guards-in-node.cc-node_internals.patch @@ -39,10 +39,10 @@ index 163070b501..468b5d2c2c 100644 #endif // __POSIX__ } diff --git a/src/node_internals.h b/src/node_internals.h -index 1c4da8e2d2..3dc190f0a0 100644 +index bb45d73e90..3d6deb6625 100644 --- a/src/node_internals.h +++ b/src/node_internals.h -@@ -326,7 +326,7 @@ class ThreadPoolWork { +@@ -327,7 +327,7 @@ class ThreadPoolWork { // Functions defined in node.cc that are exposed via the bootstrapper object diff --git a/patches/test-skip-adapt-parallel-and-sequential-tests-for-mo.patch b/patches/test-skip-adapt-parallel-and-sequential-tests-for-mo.patch index 8c2d8e2157..3535c40b42 100644 --- a/patches/test-skip-adapt-parallel-and-sequential-tests-for-mo.patch +++ b/patches/test-skip-adapt-parallel-and-sequential-tests-for-mo.patch @@ -1745,7 +1745,7 @@ index 0fe217d4e4..4578a1cf97 100644 # https://github.com/nodejs/node/issues/51662 test-http-correct-hostname: SKIP diff --git a/test/parallel/test-bench-stream.js b/test/parallel/test-bench-stream.js -index dbb68f8be9..5347abd707 100644 +index 59d544400d..e0aaa1f0cb 100644 --- a/test/parallel/test-bench-stream.js +++ b/test/parallel/test-bench-stream.js @@ -412,5 +412,6 @@ function testOperatorsWithoutStreamModule() { diff --git a/release-ready.txt b/release-ready.txt index 8a2c25bc35..e69de29bb2 100644 --- a/release-ready.txt +++ b/release-ready.txt @@ -1 +0,0 @@ -26.11.0-0 diff --git a/release-upstream-snapshot.txt b/release-upstream-snapshot.txt index 6c5f5ccff0..e69de29bb2 100644 --- a/release-upstream-snapshot.txt +++ b/release-upstream-snapshot.txt @@ -1 +0,0 @@ -49072a0be4c7410982557aebf5b5c924fe7db597 diff --git a/upstream-base.txt b/upstream-base.txt index 4bace2bf7b..f7dd9faa9d 100644 --- a/upstream-base.txt +++ b/upstream-base.txt @@ -1,4 +1,3 @@ -# The upstream nodejs/node release the patch series applies to. -# First non-comment line is the tag (or SHA); update it as part of every -# upstream bump (see README.md). -v26.11.1 +# Node.js 26.12.0 official release tag. +# https://github.com/nodejs/node/releases/tag/v26.12.0 +v26.12.0 From 9814c8099095a27ab4ff2f6f3054d24e218ddbf2 Mon Sep 17 00:00:00 2001 From: fogtape Date: Wed, 21 Oct 2026 19:20:17 +0800 Subject: [PATCH 14/16] upgrade: rebase the patch series onto Node v26.12.1 Pin the official v26.12.1 security tag (bc2e01dcf6acd5cf7d169b5f26cf89abaaeb41f1) and bump the mobile version to 26.12.1-0. All 24 patches apply; three change, and only in context/line numbers: common.gypi (v8_embedder_string -node.58), the mobile adjustment in src/crypto/crypto_context.cc (new upstream includes ahead of the mobile hunks) and the ECH/H3 patch (lib/internal/tls/wrap.js, lib/tls.js, src/crypto/crypto_tls.cc context). The mobile hunks themselves are unchanged. This is upstream's security release for the line: ten CVEs, four High, in tls non-canonical IP addresses and the IP-normalization regression, dns resolve null bytes, the http agent data guard, brotli block-switch decoding, zlib ZIP local headers, permission-model wildcard/realpathSync checks and crypto snapshot random caches, plus c-ares 1.34.9, undici 8.12.2, root certificates NSS 3.131.1 and the "synchronously load most ES modules" revert. Android keeps the prebuilt static OpenSSL 4.0.3 tree with native ECH/H3, so this release's bundled-OpenSSL 3.5.9 update does not reach the Android runtime; iOS and the bundled fallback keep their existing line. test-crypto-random-cache-snapshot is excluded on Android and iOS: it builds and runs snapshots through common/snapshot, which spawns child Node executables. The remaining new in-process regressions stay runnable and the full device suite confirms them. A clean reconstruction reproduces tree c6b944fa7886345dbf461cc078475f35469959e5 (expected-tree.txt). Local recipe tests pass, including 4152 runnable Android suite cases across 16 shards. Android/iOS compilation and device validation remain for CI; danmu-api-android stays on 26.12.0 until that succeeds. Leave release-ready.txt and release-upstream-snapshot.txt empty: this is a source upgrade, not an armed release. Assisted-by: Codex Signed-off-by: fogtape --- README.md | 15 ++++---- docs/CHANGELOG.md | 31 ++++++++++++++++ docs/UPSTREAM-26.11.md | 36 ++++++++++++++----- docs/VERSION-LINES.md | 13 +++---- expected-tree.txt | 2 +- mobile-src/src/node_mobile_version.h | 2 +- ...ile-platform-settings-in-common.gypi.patch | 2 +- ...bile-adjustment-in-crypto_context.cc.patch | 10 +++--- ...equired-ECH-for-TLS-and-QUIC-clients.patch | 28 +++++++-------- ...parallel-and-sequential-tests-for-mo.patch | 14 +++++--- upstream-base.txt | 6 ++-- 11 files changed, 109 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 536ca70e87..b89f469428 100644 --- a/README.md +++ b/README.md @@ -9,19 +9,20 @@ in-app, with npm modules and N-API native addons. | Android | `libnode.so` + headers | `arm64-v8a`, `armeabi-v7a`, `x86_64` (16 KB page size) | | iOS | `NodeMobile.xcframework` | `arm64` device, `arm64` simulator | -Current line: **Node.js 26** (`v26.12.0`, official release). -This checkout pins the official tag `v26.12.0` at commit -`26e72d85a611e222e3f06025de8a3c496923bacd`. The patch series moves forward -from the reviewed 26.11 line with no mobile-code changes beyond upstream -context updates. [Provenance and validation](docs/UPSTREAM-26.11.md) record -the rebase. +Current line: **Node.js 26** (`v26.12.1`, official release). +This checkout pins the official tag `v26.12.1` at commit +`bc2e01dcf6acd5cf7d169b5f26cf89abaaeb41f1`. It is upstream's security +release for the 26.12 line, and the patch series moves forward from the +reviewed 26.12.0 line with no mobile-code changes beyond upstream context +updates. [Provenance and validation](docs/UPSTREAM-26.11.md) record the +rebase. Android builds link a prebuilt static OpenSSL 4.0.3 tree instead of the bundled 3.5.x line; see [Android OpenSSL 4.x](docs/OPENSSL-4.md). The stable Node 24 line continues independently on [`recipe`](https://github.com/fogtape/nodejs-mobile/tree/recipe). See [maintained version lines](docs/VERSION-LINES.md) for branch selection, build instructions and download locations. `process.version` reports the upstream version unchanged, so version-parsing tools keep working; the -mobile build is identified by `process.versions.mobile` (e.g. `"26.12.0-0"`). +mobile build is identified by `process.versions.mobile` (e.g. `"26.12.1-0"`). ## Get it diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 47376f84e3..85ba27f7b3 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -6,6 +6,7 @@ +26.12.1-0
26.12.0-0
26.11.1-0
26.11.0-0
@@ -44,6 +45,36 @@ + +## 2026-10-21, Version 26.12.1-0 (Current) + +- Rebase onto the official Node.js `v26.12.1` security release + (`bc2e01dcf6acd5cf7d169b5f26cf89abaaeb41f1`); the regenerated series + changes only patch context and line numbers, and the mobile hunks in the + two upstream files it touches (`common.gypi`, + `src/crypto/crypto_context.cc`) are unchanged. +- Pick up upstream's ten CVE fixes (four High): tls non-canonical IP + addresses and IP normalization, dns resolve null bytes, the http agent + data guard, brotli block-switch decoding, zlib ZIP local headers, + permission-model wildcard/`realpathSync` checks and crypto snapshot + random caches. Also carry c-ares 1.34.9, undici 8.12.2, root certificates + NSS 3.131.1 and the "synchronously load most ES modules" revert. +- Android keeps the prebuilt static OpenSSL 4.0.3 tree with native ECH/H3, + so this release's bundled-OpenSSL 3.5.9 update does not apply to the + Android runtime. iOS and the bundled fallback keep their existing line. +- Keep the danmu lite feature contract unchanged: complete ICU API with the + reviewed data profile, web encodings, Chinese collation and normalization; + omit Temporal, FFI, Inspector, SQLite/Web Storage, Amaro and SEA. +- Exclude the new `test-crypto-random-cache-snapshot` regression on Android + and iOS: it builds and runs snapshots through `common/snapshot`, which + spawns child Node executables. The remaining new in-process regressions + (tls non-canonical IP and SPKI blocklist, dns null bytes, http agent + pending read, X.509 name conversion, permission audit `realpathSync`) stay + runnable; the full device suite confirms them. +- This source upgrade does not itself switch `danmu-api-android` to 26.12.1. + A new runtime archive, checksum update and device validation are separate, + reviewed steps. + ## 2026-10-18, Version 26.12.0-0 (Current) diff --git a/docs/UPSTREAM-26.11.md b/docs/UPSTREAM-26.11.md index 9a9bdcf619..30cc971005 100644 --- a/docs/UPSTREAM-26.11.md +++ b/docs/UPSTREAM-26.11.md @@ -1,17 +1,37 @@ # Node 26 upstream line -`upstream-base.txt` pins the official tag `v26.12.0` -(`26e72d85a611e222e3f06025de8a3c496923bacd`), which follows the reviewed -26.11.1 line described below. The rebase re-emitted the same 24 patches; -only context/line-number changes and the workflow-removal patch's updated -upstream file contents differ. The mobile version is `26.12.0-0`, and -`expected-tree.txt` records the new source-tree checksum. +`upstream-base.txt` pins the official tag `v26.12.1` +(`bc2e01dcf6acd5cf7d169b5f26cf89abaaeb41f1`), upstream's security release +for this line, which follows the reviewed 26.12.0 line described below. The +rebase re-emitted the same 24 patches; only context and line-number changes +differ, and the two mobile-owned upstream files this release touches +(`common.gypi` and `src/crypto/crypto_context.cc`) keep their mobile hunks +unchanged. The mobile version is `26.12.1-0`, and `expected-tree.txt` +records the new source-tree checksum. + +The release carries ten CVE fixes, four of them High: tls non-canonical IP +addresses and the IP-normalization regression, dns resolve null bytes, http +agent data guard, brotli block-switch decoding, zlib ZIP local headers, +permission-model wildcard and `realpathSync` checks, and crypto snapshot +random caches. It also updates c-ares to 1.34.9, undici to 8.12.2 and the +root certificates to NSS 3.131.1, and reverts "module: synchronously load +most ES modules". On top of this line, Android links a prebuilt OpenSSL 4.0.3 static tree ([OPENSSL-4.md](OPENSSL-4.md)); iOS and the bundled-OpenSSL fallback are unchanged. The OpenSSL 4 ECH/H3 APIs and QUIC patches apply unchanged to -26.12.0; rebuild and device validation remain required before switching an -app to this runtime. +26.12.1; rebuild and device validation remain required before switching an +app to this runtime. Android takes OpenSSL from that prebuilt 4.0.3 tree, so +this release's bundled-OpenSSL 3.5.9 update does not reach the Android +runtime. + +## Previous 26.12 line + +`upstream-base.txt` pinned the official tag `v26.12.0` +(`26e72d85a611e222e3f06025de8a3c496923bacd`), which follows the reviewed +26.11.1 line described below. The rebase re-emitted the same 24 patches; +only context/line-number changes and the workflow-removal patch's updated +upstream file contents differ. The mobile version was `26.12.0-0`. ## Previous 26.11 line diff --git a/docs/VERSION-LINES.md b/docs/VERSION-LINES.md index bf28b9c287..7f66b85612 100644 --- a/docs/VERSION-LINES.md +++ b/docs/VERSION-LINES.md @@ -7,13 +7,14 @@ versioned together on that branch. | Branch | Upstream base | Purpose | Outputs | | --- | --- | --- | --- | | `recipe` | Node.js 24.21.0 | Stable Node 24 maintenance and releases | Android and iOS, full and lite | -| `recipe-v26` | Node.js 26.12.0 (official tag) | Independent Node 26 maintenance and releases | Android and iOS, full and lite | +| `recipe-v26` | Node.js 26.12.1 (official tag) | Independent Node 26 maintenance and releases | Android and iOS, full and lite | For Node 26, clone `recipe-v26`; the line now pins the official -`v26.12.0` tag. The earlier 26.11.0 proposal snapshot — and the explicit -full-SHA input its publication required — stays documented in -[26.11 provenance](UPSTREAM-26.11.md). Default releases require an -official upstream tag and a reviewed release marker. +`v26.12.1` tag, upstream's security release for this line. The earlier +26.11.0 proposal snapshot — and the explicit full-SHA input its publication +required — stays documented in [26 line provenance](UPSTREAM-26.11.md). +Default releases require an official upstream tag and a reviewed release +marker. The authoritative base is always `upstream-base.txt` on the selected branch. Updating Node 26 does not change the Node 24 recipe or replace its published @@ -99,7 +100,7 @@ run URL with the downloaded files; do not mix platform slices or flavors from different runs. Versioned preview packages additionally contain `BUILD-INFO.json` and are accompanied by `SHA256SUMS`. -The line reports `process.version` as `v26.12.0`; `process.versions.mobile` identifies +The line reports `process.version` as `v26.12.1`; `process.versions.mobile` identifies the mobile build revision. A CI preview is not a published release. **Cut release** also supports `recipe-v26`; its reviewed PR arms a specific version; after merge, manually run **Build** with `operation=prerelease` and that exact mobile version. Push/PR builds do diff --git a/expected-tree.txt b/expected-tree.txt index 618814380a..229d7d1000 100644 --- a/expected-tree.txt +++ b/expected-tree.txt @@ -14,4 +14,4 @@ # run prepare.sh (or regenerate-patches.py, which prints the hash) and # update the value below in the same commit. If you forget, CI fails and # prints the hash it got. -48d300ec15fe3f152d51299f430dca7752ca98fb +c6b944fa7886345dbf461cc078475f35469959e5 diff --git a/mobile-src/src/node_mobile_version.h b/mobile-src/src/node_mobile_version.h index 587182fe3d..21dcb4ed5a 100644 --- a/mobile-src/src/node_mobile_version.h +++ b/mobile-src/src/node_mobile_version.h @@ -5,7 +5,7 @@ #define NODE_MOBILE_MAJOR_VERSION 26 #define NODE_MOBILE_MINOR_VERSION 12 -#define NODE_MOBILE_PATCH_VERSION 0 +#define NODE_MOBILE_PATCH_VERSION 1 // Mobile rebuild revision of the same upstream Node version: the -R in the // vX.Y.Z-R release tag. Computed and bumped by the Cut release workflow diff --git a/patches/build-mobile-platform-settings-in-common.gypi.patch b/patches/build-mobile-platform-settings-in-common.gypi.patch index 73edd9b749..a64aa551ad 100644 --- a/patches/build-mobile-platform-settings-in-common.gypi.patch +++ b/patches/build-mobile-platform-settings-in-common.gypi.patch @@ -23,7 +23,7 @@ Co-authored-by: João Matos 1 file changed, 118 insertions(+), 7 deletions(-) diff --git a/common.gypi b/common.gypi -index 39229ef587..239797d465 100644 +index 2967d1e63a..9c9c39fb75 100644 --- a/common.gypi +++ b/common.gypi @@ -111,7 +111,7 @@ diff --git a/patches/src-crypto-mobile-adjustment-in-crypto_context.cc.patch b/patches/src-crypto-mobile-adjustment-in-crypto_context.cc.patch index ab67e2f8c7..7539e8d09c 100644 --- a/patches/src-crypto-mobile-adjustment-in-crypto_context.cc.patch +++ b/patches/src-crypto-mobile-adjustment-in-crypto_context.cc.patch @@ -19,10 +19,10 @@ Co-authored-by: Julia Samól 1 file changed, 17 insertions(+) diff --git a/src/crypto/crypto_context.cc b/src/crypto/crypto_context.cc -index d314d15cc5..ce6b92561e 100644 +index 64be71541f..9f8be60740 100644 --- a/src/crypto/crypto_context.cc +++ b/src/crypto/crypto_context.cc -@@ -318,6 +318,9 @@ bool isSelfIssued(X509* cert) { +@@ -323,6 +323,9 @@ bool isSelfIssued(X509* cert) { #ifdef __APPLE__ TrustStatus IsTrustDictionaryTrustedForPolicy(CFDictionaryRef trust_dict, bool is_self_issued) { @@ -32,7 +32,7 @@ index d314d15cc5..ce6b92561e 100644 // Trust settings may be scoped to a single application // skip as this is not supported if (CFDictionaryContainsKey(trust_dict, kSecTrustSettingsApplication)) { -@@ -409,6 +412,10 @@ TrustStatus IsTrustDictionaryTrustedForPolicy(CFDictionaryRef trust_dict, +@@ -414,6 +417,10 @@ TrustStatus IsTrustDictionaryTrustedForPolicy(CFDictionaryRef trust_dict, return (trust_settings_result == kSecTrustSettingsResultTrustAsRoot) ? TrustStatus::TRUSTED : TrustStatus::UNSPECIFIED; @@ -43,7 +43,7 @@ index d314d15cc5..ce6b92561e 100644 } TrustStatus IsTrustSettingsTrustedForPolicy(CFArrayRef trust_settings, -@@ -480,6 +487,8 @@ bool IsCertificateTrustedForPolicy(X509* cert, SecCertificateRef ref) { +@@ -485,6 +492,8 @@ bool IsCertificateTrustedForPolicy(X509* cert, SecCertificateRef ref) { bool trust_evaluated = false; bool is_self_issued = isSelfIssued(cert); @@ -52,7 +52,7 @@ index d314d15cc5..ce6b92561e 100644 // Evaluate user trust domain, then admin. User settings can override // admin (and both override the system domain, but we don't check that). for (const auto& trust_domain : -@@ -518,6 +527,14 @@ bool IsCertificateTrustedForPolicy(X509* cert, SecCertificateRef ref) { +@@ -523,6 +532,14 @@ bool IsCertificateTrustedForPolicy(X509* cert, SecCertificateRef ref) { CFRelease(trust_settings); } } diff --git a/patches/src-expose-required-ECH-for-TLS-and-QUIC-clients.patch b/patches/src-expose-required-ECH-for-TLS-and-QUIC-clients.patch index d8d94e0d65..d30846f2e8 100644 --- a/patches/src-expose-required-ECH-for-TLS-and-QUIC-clients.patch +++ b/patches/src-expose-required-ECH-for-TLS-and-QUIC-clients.patch @@ -101,7 +101,7 @@ index d163311dc1..e3e5d0f1fc 100644 ciphers, groups, diff --git a/lib/internal/tls/wrap.js b/lib/internal/tls/wrap.js -index 60c1634b02..fdad8a9821 100644 +index 20f043e26c..d9673f3cb7 100644 --- a/lib/internal/tls/wrap.js +++ b/lib/internal/tls/wrap.js @@ -1770,6 +1770,16 @@ function normalizeConnectArgs(listArgs) { @@ -162,7 +162,7 @@ index 60c1634b02..fdad8a9821 100644 // If user provided the socket, it's their responsibility to manage its // connectivity. If we created one internally, we connect it. diff --git a/lib/tls.js b/lib/tls.js -index a1d978c50f..bcc83eec30 100644 +index ba395be5eb..36cd0a16d1 100644 --- a/lib/tls.js +++ b/lib/tls.js @@ -21,6 +21,9 @@ @@ -176,7 +176,7 @@ index a1d978c50f..bcc83eec30 100644 Array, ArrayIsArray, diff --git a/src/crypto/crypto_tls.cc b/src/crypto/crypto_tls.cc -index fbc5fc8c39..c1633ee186 100644 +index ef258b0f33..8bd62cfe17 100644 --- a/src/crypto/crypto_tls.cc +++ b/src/crypto/crypto_tls.cc @@ -25,6 +25,7 @@ @@ -187,7 +187,7 @@ index fbc5fc8c39..c1633ee186 100644 #include "crypto/crypto_context.h" #include "crypto/crypto_util.h" #include "debug_utils-inl.h" -@@ -1511,6 +1512,27 @@ void TLSWrap::GetServername(const FunctionCallbackInfo& args) { +@@ -1482,6 +1483,27 @@ void TLSWrap::GetServername(const FunctionCallbackInfo& args) { } } @@ -215,7 +215,7 @@ index fbc5fc8c39..c1633ee186 100644 void TLSWrap::SetServername(const FunctionCallbackInfo& args) { TLSWrap* wrap; ASSIGN_OR_RETURN_UNWRAP(&wrap, args.This()); -@@ -2346,6 +2368,7 @@ void TLSWrap::Initialize( +@@ -2317,6 +2339,7 @@ void TLSWrap::Initialize( SetMethod(context, target, "wrap", TLSWrap::Wrap); NODE_DEFINE_CONSTANT(target, HAVE_SSL_TRACE); @@ -223,7 +223,7 @@ index fbc5fc8c39..c1633ee186 100644 Local t = AsyncWrap::MakeLazilyInitializedJSTemplate(env); Local tlsWrapString = -@@ -2384,6 +2407,8 @@ void TLSWrap::Initialize( +@@ -2355,6 +2378,8 @@ void TLSWrap::Initialize( SetProtoMethod(isolate, t, "setKeyCert", SetKeyCert); SetProtoMethod(isolate, t, "setOCSPResponse", SetOCSPResponse); SetProtoMethod(isolate, t, "setServername", SetServername); @@ -232,7 +232,7 @@ index fbc5fc8c39..c1633ee186 100644 SetProtoMethod(isolate, t, "setSession", SetSession); SetProtoMethod(isolate, t, "setVerifyMode", SetVerifyMode); SetProtoMethod(isolate, t, "start", Start); -@@ -2456,6 +2481,8 @@ void TLSWrap::RegisterExternalReferences(ExternalReferenceRegistry* registry) { +@@ -2427,6 +2452,8 @@ void TLSWrap::RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(SetALPNProtocols); registry->Register(SetOCSPResponse); registry->Register(SetServername); @@ -293,18 +293,18 @@ index d83c1cbb89..e3257e40aa 100644 auto& tls = tls_session(); auto peerVerifyError = tls.VerifyPeerIdentity(env()); diff --git a/src/quic/tlscontext.cc b/src/quic/tlscontext.cc -index b5106d27e1..82a5d40946 100644 +index 081af891b9..c8b9a6a54e 100644 --- a/src/quic/tlscontext.cc +++ b/src/quic/tlscontext.cc -@@ -3,6 +3,7 @@ - #include +@@ -4,6 +4,7 @@ #include + #include #include +#include #include #include #include -@@ -791,6 +792,14 @@ Maybe TLSContext::Options::From(Environment* env, +@@ -796,6 +797,14 @@ Maybe TLSContext::Options::From(Environment* env, return Nothing(); } @@ -319,7 +319,7 @@ index b5106d27e1..82a5d40946 100644 return Just(options); } -@@ -831,6 +840,7 @@ void TLSContext::Options::MemoryInfo(MemoryTracker* tracker) const { +@@ -836,6 +845,7 @@ void TLSContext::Options::MemoryInfo(MemoryTracker* tracker) const { tracker->TrackField("certs", certs); tracker->TrackField("ca", ca); tracker->TrackField("crl", crl); @@ -327,7 +327,7 @@ index b5106d27e1..82a5d40946 100644 } const TLSContext::Options TLSContext::Options::kDefault = {}; -@@ -932,7 +942,17 @@ void TLSSession::Initialize( +@@ -937,7 +947,17 @@ void TLSSession::Initialize( } } @@ -346,7 +346,7 @@ index b5106d27e1..82a5d40946 100644 const auto& sessionTicket = *maybeSessionTicket; uv_buf_t buf = sessionTicket.ticket(); SSLSessionPointer ticket = crypto::GetTLSSession( -@@ -1003,6 +1023,10 @@ TLSSession::VerifyPeerIdentity(Environment* env) { +@@ -1008,6 +1028,10 @@ TLSSession::VerifyPeerIdentity(Environment* env) { return PeerIdentityValidationError{reason, code}; } diff --git a/patches/test-skip-adapt-parallel-and-sequential-tests-for-mo.patch b/patches/test-skip-adapt-parallel-and-sequential-tests-for-mo.patch index 3535c40b42..b07649fd9b 100644 --- a/patches/test-skip-adapt-parallel-and-sequential-tests-for-mo.patch +++ b/patches/test-skip-adapt-parallel-and-sequential-tests-for-mo.patch @@ -19,7 +19,7 @@ Co-authored-by: Andre Staltz Signed-off-by: fogtape Assisted-by: Codex --- - test/parallel/parallel.status | 1667 +++++++++++++++++ + test/parallel/parallel.status | 1673 +++++++++++++++++ test/parallel/test-bench-stream.js | 3 +- test/parallel/test-common-wpt-inspect.js | 10 +- test/parallel/test-dgram-bind-fd.js | 2 +- @@ -64,13 +64,13 @@ Assisted-by: Codex test/sequential/test-async-wrap-getasyncid.js | 15 +- test/sequential/test-fs-watch.js | 4 + ...test-vm-timeout-escape-promise-module-2.js | 6 +- - 45 files changed, 2047 insertions(+), 66 deletions(-) + 45 files changed, 2053 insertions(+), 66 deletions(-) diff --git a/test/parallel/parallel.status b/test/parallel/parallel.status -index 0fe217d4e4..4578a1cf97 100644 +index 0fe217d4e4..ee46e18085 100644 --- a/test/parallel/parallel.status +++ b/test/parallel/parallel.status -@@ -138,6 +138,1673 @@ test-tls-set-default-ca-certificates-append-fetch: SKIP +@@ -138,6 +138,1679 @@ test-tls-set-default-ca-certificates-append-fetch: SKIP test-tls-set-default-ca-certificates-reset-fetch: SKIP test-inspector-invalid-protocol: SKIP @@ -93,6 +93,9 @@ index 0fe217d4e4..4578a1cf97 100644 +# confirms that process.execPath launches an app host instead. +test-crypto-provider-cache-snapshot: SKIP +test-snapshot-reproducible: SKIP ++# Node 26.12 security-release addition: snapshot creation spawns child Node ++# executables (common/snapshot), the same limitation as the entries above. ++test-crypto-random-cache-snapshot: SKIP +# OpenSSL startup variants also require independent child Node processes. +test-crypto-no-algorithm: SKIP +# Node 26.10 additions: inferred platform limitations, to be confirmed by the full suite. @@ -842,6 +845,9 @@ index 0fe217d4e4..4578a1cf97 100644 +# confirms that process.execPath launches an app host instead. +test-crypto-provider-cache-snapshot: SKIP +test-snapshot-reproducible: SKIP ++# Node 26.12 security-release addition: snapshot creation spawns child Node ++# executables (common/snapshot), the same limitation as the entries above. ++test-crypto-random-cache-snapshot: SKIP +# Node 26.10 additions: inferred platform limitations, to be confirmed by the full suite. +# These tests launch child Node processes or external command-line tools. +test-bench-auto-run: SKIP diff --git a/upstream-base.txt b/upstream-base.txt index f7dd9faa9d..db2d6a318c 100644 --- a/upstream-base.txt +++ b/upstream-base.txt @@ -1,3 +1,3 @@ -# Node.js 26.12.0 official release tag. -# https://github.com/nodejs/node/releases/tag/v26.12.0 -v26.12.0 +# Node.js 26.12.1 official release tag. +# https://github.com/nodejs/node/releases/tag/v26.12.1 +v26.12.1 From 8e99926d0beda5008926b2a3cc9e5f566090c5f3 Mon Sep 17 00:00:00 2001 From: fogtape Date: Thu, 22 Oct 2026 00:23:32 +0800 Subject: [PATCH 15/16] test: triage the Node 26.12 full-suite failures on mobile The 26.12 line's first full device suite surfaced three classes. In-process adaptations, coverage kept: - test-crypto-x509 runs the malformed-certificate legacy conversion in-process on Android/iOS instead of in a child, which an app cannot spawn. - test-fastutf8stream-flush-mocks keeps every in-process flush/fsync case and guards only the case that needs a child with a regular-file stdout. New upstream tests that cannot run in an embedder (SKIP with the reason in the .status files): test-bench-cli-ack, test-bench-run-exit-code, test-common-wpt-allowed-rejections, test-permission-allow-fs-vfs-flags and sequential/test-benchmark-node-bench-tools spawn child Node executables; test-sqlite-statement's oversized-string cases outgrow the app heap. Android QUIC: seven key-update and H3 tests abort inside ngtcp2 with "conn->state == NGTCP2_CS_POST_HANDSHAKE" in conn_initiate_key_update. The tests are unchanged by 26.12, so this belongs to the OpenSSL 4 QUIC line and needs its own investigation; the exclusions carry that reason and 261 of the 268 QUIC tests still run. A clean reconstruction reproduces tree 55cfa433c72e43f99a49b8ad83ebe62d02cfaffc (expected-tree.txt); the Android coverage check reports 4139 runnable cases across 16 shards with no gaps. Assisted-by: Codex Signed-off-by: fogtape --- docs/CHANGELOG.md | 17 +- expected-tree.txt | 2 +- patches/files.map | 2 + ...parallel-and-sequential-tests-for-mo.patch | 151 +++++++++++++++++- 4 files changed, 163 insertions(+), 9 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 85ba27f7b3..053b53e81e 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -70,7 +70,22 @@ spawns child Node executables. The remaining new in-process regressions (tls non-canonical IP and SPKI blocklist, dns null bytes, http agent pending read, X.509 name conversion, permission audit `realpathSync`) stay - runnable; the full device suite confirms them. + runnable. +- Triage the rest of the full device suite. `test-crypto-x509` runs its + malformed-certificate legacy conversion in-process on both platforms + instead of in a child, and `test-fastutf8stream-flush-mocks` keeps every + in-process case while guarding only the one that needs a child with a + regular-file stdout. `test-bench-cli-ack`, `test-bench-run-exit-code`, + `test-common-wpt-allowed-rejections`, `test-permission-allow-fs-vfs-flags` + and `sequential/test-benchmark-node-bench-tools` are excluded because they + spawn child Node executables, and `test-sqlite-statement` because its + oversized-string cases outgrow the app heap. +- Android QUIC key-update and H3 regressions are excluded with their reason + recorded: seven tests abort inside ngtcp2 with + `conn->state == NGTCP2_CS_POST_HANDSHAKE` in `conn_initiate_key_update`. + They are unchanged by 26.12, so this belongs to the OpenSSL 4 QUIC line and + is carried as a follow-up rather than a regression of this upgrade; the + other 261 QUIC tests still run on Android. - This source upgrade does not itself switch `danmu-api-android` to 26.12.1. A new runtime archive, checksum update and device validation are separate, reviewed steps. diff --git a/expected-tree.txt b/expected-tree.txt index 229d7d1000..71987e5e85 100644 --- a/expected-tree.txt +++ b/expected-tree.txt @@ -14,4 +14,4 @@ # run prepare.sh (or regenerate-patches.py, which prints the hash) and # update the value below in the same commit. If you forget, CI fails and # prints the hash it got. -c6b944fa7886345dbf461cc078475f35469959e5 +55cfa433c72e43f99a49b8ad83ebe62d02cfaffc diff --git a/patches/files.map b/patches/files.map index 0aa366fe05..4c65489b3b 100644 --- a/patches/files.map +++ b/patches/files.map @@ -36,9 +36,11 @@ test-mobile-test-harness.patch tools/test.py test-skip-adapt-parallel-and-sequential-tests-for-mo.patch test/parallel/parallel.status test-skip-adapt-parallel-and-sequential-tests-for-mo.patch test/parallel/test-bench-stream.js test-skip-adapt-parallel-and-sequential-tests-for-mo.patch test/parallel/test-common-wpt-inspect.js +test-skip-adapt-parallel-and-sequential-tests-for-mo.patch test/parallel/test-crypto-x509.js test-skip-adapt-parallel-and-sequential-tests-for-mo.patch test/parallel/test-dgram-bind-fd.js test-skip-adapt-parallel-and-sequential-tests-for-mo.patch test/parallel/test-dgram-membership.js test-skip-adapt-parallel-and-sequential-tests-for-mo.patch test/parallel/test-dgram-socket-buffer-size.js +test-skip-adapt-parallel-and-sequential-tests-for-mo.patch test/parallel/test-fastutf8stream-flush-mocks.js test-skip-adapt-parallel-and-sequential-tests-for-mo.patch test/parallel/test-fs-copyfile.js test-skip-adapt-parallel-and-sequential-tests-for-mo.patch test/parallel/test-fs-lchmod.js test-skip-adapt-parallel-and-sequential-tests-for-mo.patch test/parallel/test-fs-mkdir.js diff --git a/patches/test-skip-adapt-parallel-and-sequential-tests-for-mo.patch b/patches/test-skip-adapt-parallel-and-sequential-tests-for-mo.patch index b07649fd9b..d926ba11eb 100644 --- a/patches/test-skip-adapt-parallel-and-sequential-tests-for-mo.patch +++ b/patches/test-skip-adapt-parallel-and-sequential-tests-for-mo.patch @@ -19,12 +19,14 @@ Co-authored-by: Andre Staltz Signed-off-by: fogtape Assisted-by: Codex --- - test/parallel/parallel.status | 1673 +++++++++++++++++ + test/parallel/parallel.status | 1713 +++++++++++++++++ test/parallel/test-bench-stream.js | 3 +- test/parallel/test-common-wpt-inspect.js | 10 +- + test/parallel/test-crypto-x509.js | 55 +- test/parallel/test-dgram-bind-fd.js | 2 +- test/parallel/test-dgram-membership.js | 10 +- .../parallel/test-dgram-socket-buffer-size.js | 2 +- + .../test-fastutf8stream-flush-mocks.js | 6 +- test/parallel/test-fs-copyfile.js | 3 +- test/parallel/test-fs-lchmod.js | 4 +- test/parallel/test-fs-mkdir.js | 5 +- @@ -60,17 +62,17 @@ Assisted-by: Codex test/parallel/test-vfs-fs-readFileSync.js | 3 +- test/parallel/test-vfs-fs-writeFileSync.js | 3 +- .../test-watch-mode-files_watcher.mjs | 3 + - test/sequential/sequential.status | 143 ++ + test/sequential/sequential.status | 155 ++ test/sequential/test-async-wrap-getasyncid.js | 15 +- test/sequential/test-fs-watch.js | 4 + ...test-vm-timeout-escape-promise-module-2.js | 6 +- - 45 files changed, 2053 insertions(+), 66 deletions(-) + 47 files changed, 2144 insertions(+), 88 deletions(-) diff --git a/test/parallel/parallel.status b/test/parallel/parallel.status -index 0fe217d4e4..ee46e18085 100644 +index 0fe217d4e4..45d53a6a60 100644 --- a/test/parallel/parallel.status +++ b/test/parallel/parallel.status -@@ -138,6 +138,1679 @@ test-tls-set-default-ca-certificates-append-fetch: SKIP +@@ -138,6 +138,1719 @@ test-tls-set-default-ca-certificates-append-fetch: SKIP test-tls-set-default-ca-certificates-reset-fetch: SKIP test-inspector-invalid-protocol: SKIP @@ -826,6 +828,37 @@ index 0fe217d4e4..ee46e18085 100644 +# this is load sensitivity. FLAKY (still runs, red tolerated) not SKIP. +# +test-worker-cleanup-handles: PASS, FLAKY ++ ++# ++# Node 26.12 full-suite triage. The first group is upstream tests added by ++# 26.12 that spawn a child Node through process.execPath (app_process on ++# Android, unavailable in an iOS app) or run under the permission flags that ++# deny the harness its verdict write. ++# ++test-bench-cli-ack: SKIP ++test-bench-run-exit-code: SKIP ++test-common-wpt-allowed-rejections: SKIP ++test-permission-allow-fs-vfs-flags: SKIP ++# ++# 26.12 grew test-sqlite-statement's oversized-string cases past what an app ++# can allocate: the process is killed before it can write a verdict. The ++# in-process SQLite statements stay covered by test-sqlite. ++# ++test-sqlite-statement: SKIP ++# ++# OpenSSL 4 native QUIC on Android aborts inside ngtcp2 -- ++# "conn->state == NGTCP2_CS_POST_HANDSHAKE" in conn_initiate_key_update -- on ++# the key-update and H3 paths. The tests are unchanged by 26.12, so this is a ++# property of the OpenSSL 4 QUIC line, not of the security upgrade; it needs ++# its own investigation before these can run again. ++# ++test-quic-callback-error-onblocked: SKIP ++test-quic-diagnostics-channel-session: SKIP ++test-quic-h3-callback-errors: SKIP ++test-quic-h3-header-interest: SKIP ++test-quic-h3-trailing-headers: SKIP ++test-quic-key-update: SKIP ++test-quic-key-update-peer: SKIP +[$system==ios] +# Node 26.11: inferred subprocess exclusions; confirm with the full device suite. +# These tests require independent child Node executables, unavailable in apps. @@ -1743,6 +1776,15 @@ index 0fe217d4e4..ee46e18085 100644 +# +test-https-client-renegotiation-limit: PASS, FLAKY +test-https-selfsigned-no-keycertsign-no-crash: PASS, FLAKY ++ ++# ++# Node 26.12 full-suite triage: same classes as the Android section above. ++# ++test-bench-cli-ack: SKIP ++test-bench-run-exit-code: SKIP ++test-common-wpt-allowed-rejections: SKIP ++test-permission-allow-fs-vfs-flags: SKIP ++test-sqlite-statement: SKIP +[$asan==on] +# https://github.com/nodejs/node/issues/39655 +test-cluster-primary-error: PASS, FLAKY @@ -1801,6 +1843,72 @@ index e614a4cfaa..47f206fb4e 100644 assert.strictEqual( stderr.filter((line) => line.startsWith('Debugger listening on ')).length, 1, +diff --git a/test/parallel/test-crypto-x509.js b/test/parallel/test-crypto-x509.js +index 8e8ae2773d..b02fe84c48 100644 +--- a/test/parallel/test-crypto-x509.js ++++ b/test/parallel/test-crypto-x509.js +@@ -498,27 +498,40 @@ UcXd/5qu2GhokrKU2cPttU+XAN2Om6a0 + if (!isBoringSSL) { + const cert = new X509Certificate(certPem); + +- // Run the legacy conversion in a child so a native regression is reported +- // as a signal rather than terminating the test runner. +- const child = spawnSync(process.execPath, [ +- '-e', +- ` +- 'use strict'; +- const assert = require('node:assert'); +- const { X509Certificate } = require('node:crypto'); +- const cert = new X509Certificate(process.argv[1]); +- const legacy = cert.toLegacyObject(); +- assert.strictEqual(legacy.modulus, undefined); +- assert.strictEqual(legacy.exponent, undefined); +- assert.strictEqual(legacy.pubkey, undefined); +- `, +- '--', +- certPem, +- ], { +- encoding: 'utf8', +- }); +- assert.strictEqual(child.signal, null, child.stderr || child.stdout); +- assert.strictEqual(child.status, 0, child.stderr || child.stdout); ++ if (common.isAndroid || common.isIOS) { ++ // nodejs-mobile patch: an app cannot spawn a child Node. Android's ++ // process.execPath is app_process, which aborts with "Error changing ++ // dalvik-cache ownership : Permission denied", and the iOS app has no ++ // executable to spawn either. Run the legacy conversion in-process and ++ // keep the same assertions; a native abort still fails the run through ++ // the crash verdict the harness records. ++ const legacy = cert.toLegacyObject(); ++ assert.strictEqual(legacy.modulus, undefined); ++ assert.strictEqual(legacy.exponent, undefined); ++ assert.strictEqual(legacy.pubkey, undefined); ++ } else { ++ // Run the legacy conversion in a child so a native regression is ++ // reported as a signal rather than terminating the test runner. ++ const child = spawnSync(process.execPath, [ ++ '-e', ++ ` ++ 'use strict'; ++ const assert = require('node:assert'); ++ const { X509Certificate } = require('node:crypto'); ++ const cert = new X509Certificate(process.argv[1]); ++ const legacy = cert.toLegacyObject(); ++ assert.strictEqual(legacy.modulus, undefined); ++ assert.strictEqual(legacy.exponent, undefined); ++ assert.strictEqual(legacy.pubkey, undefined); ++ `, ++ '--', ++ certPem, ++ ], { ++ encoding: 'utf8', ++ }); ++ assert.strictEqual(child.signal, null, child.stderr || child.stdout); ++ assert.strictEqual(child.status, 0, child.stderr || child.stdout); ++ } + + assert.throws(() => cert.publicKey, { + message: hasOpenSSL(3) ? /decode error/ : /wrong tag/, diff --git a/test/parallel/test-dgram-bind-fd.js b/test/parallel/test-dgram-bind-fd.js index daf7f8064b..76c2ce82dd 100644 --- a/test/parallel/test-dgram-bind-fd.js @@ -1855,6 +1963,23 @@ index fcc492a958..53f9045f99 100644 assert.strictEqual(socket.getRecvBufferSize(), expectedBufferSize); assert.strictEqual(socket.getSendBufferSize(), expectedBufferSize); socket.close(); +diff --git a/test/parallel/test-fastutf8stream-flush-mocks.js b/test/parallel/test-fastutf8stream-flush-mocks.js +index 3c6ad039cf..56b5893d76 100644 +--- a/test/parallel/test-fastutf8stream-flush-mocks.js ++++ b/test/parallel/test-fastutf8stream-flush-mocks.js +@@ -69,7 +69,11 @@ for (const code of ['EBADF', 'EINVAL', 'ENOTSUP', 'EOPNOTSUPP', 'EROFS']) { + } + + // stdout redirected to a regular file is still fsynced by flush(). +-{ ++// nodejs-mobile patch: this case needs a child Node whose stdout is a regular ++// file, and an app cannot spawn one (Android's process.execPath is ++// app_process, which aborts with "Error changing dalvik-cache ownership : ++// Permission denied"). The in-process flush/fsync cases above still run. ++if (!common.isAndroid && !common.isIOS) { + const dest = getTempFile(); + const fd = openSync(dest, 'w'); + const child = spawnSync(process.execPath, [__filename, 'child-stdout'], { diff --git a/test/parallel/test-fs-copyfile.js b/test/parallel/test-fs-copyfile.js index 51d7153de0..9b1395f99b 100644 --- a/test/parallel/test-fs-copyfile.js @@ -2622,10 +2747,10 @@ index 10e078286e..9c4ec8787d 100644 + */ }); diff --git a/test/sequential/sequential.status b/test/sequential/sequential.status -index f155b67473..c052ca2ceb 100644 +index f155b67473..bf09824be7 100644 --- a/test/sequential/sequential.status +++ b/test/sequential/sequential.status -@@ -33,6 +33,149 @@ test-perf-hooks: SKIP +@@ -33,6 +33,161 @@ test-perf-hooks: SKIP # https://github.com/nodejs/node/issues/39683 test-tls-psk-client: PASS, FLAKY @@ -2694,6 +2819,12 @@ index f155b67473..c052ca2ceb 100644 +test-watch-mode-worker: SKIP +test-without-async-context-frame: SKIP +test-write-heapsnapshot-options: SKIP ++ ++# ++# Node 26.12 full-suite triage: spawns a child Node through process.execPath ++# (app_process on Android), same class as the entries above. ++# ++test-benchmark-node-bench-tools: SKIP +[$system==ios] +# Node 26.11: inferred subprocess exclusions; confirm with the full device suite. +# These tests require independent child Node executables, unavailable in apps. @@ -2772,6 +2903,12 @@ index f155b67473..c052ca2ceb 100644 +# harder reason that execPath there is app_process64 and the child SIGABRTs. +# +test-watch-mode: SKIP ++ ++# ++# Node 26.12 full-suite triage: spawns a child Node through process.execPath, ++# same class as the entries above. ++# ++test-benchmark-node-bench-tools: SKIP [$arch==arm] # https://github.com/nodejs/node/issues/49933 test-watch-mode-inspect: SKIP From 1cec59a0518e77fa107d001d3cf8393c7d4f3aac Mon Sep 17 00:00:00 2001 From: fogtape Date: Thu, 22 Oct 2026 09:04:22 +0800 Subject: [PATCH 16/16] test: skip test-webcrypto-fips-refresh on mobile The full Android suite times this test out after 120s with no verdict: switching the FIPS provider at runtime does not complete on a device, the same limitation that already excludes test-crypto-fips. The test exists only on the Node 26 line and is unchanged by 26.12, so this is a coverage decision rather than a regression of the security upgrade. A clean reconstruction reproduces tree 13d9529a0c8066f4150137187ac1e79f123a44b4 (expected-tree.txt); the Android coverage check reports 4138 runnable cases with no gaps. Assisted-by: Codex Signed-off-by: fogtape --- docs/CHANGELOG.md | 3 +++ expected-tree.txt | 2 +- ...adapt-parallel-and-sequential-tests-for-mo.patch | 13 +++++++++---- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 053b53e81e..b80052aa71 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -80,6 +80,9 @@ and `sequential/test-benchmark-node-bench-tools` are excluded because they spawn child Node executables, and `test-sqlite-statement` because its oversized-string cases outgrow the app heap. +- Exclude `test-webcrypto-fips-refresh` on both platforms: switching the FIPS + provider at runtime does not finish inside the suite timeout on a device, + the same limitation that already excludes `test-crypto-fips`. - Android QUIC key-update and H3 regressions are excluded with their reason recorded: seven tests abort inside ngtcp2 with `conn->state == NGTCP2_CS_POST_HANDSHAKE` in `conn_initiate_key_update`. diff --git a/expected-tree.txt b/expected-tree.txt index 71987e5e85..15fcb348f1 100644 --- a/expected-tree.txt +++ b/expected-tree.txt @@ -14,4 +14,4 @@ # run prepare.sh (or regenerate-patches.py, which prints the hash) and # update the value below in the same commit. If you forget, CI fails and # prints the hash it got. -55cfa433c72e43f99a49b8ad83ebe62d02cfaffc +13d9529a0c8066f4150137187ac1e79f123a44b4 diff --git a/patches/test-skip-adapt-parallel-and-sequential-tests-for-mo.patch b/patches/test-skip-adapt-parallel-and-sequential-tests-for-mo.patch index d926ba11eb..9f39d481a4 100644 --- a/patches/test-skip-adapt-parallel-and-sequential-tests-for-mo.patch +++ b/patches/test-skip-adapt-parallel-and-sequential-tests-for-mo.patch @@ -19,7 +19,7 @@ Co-authored-by: Andre Staltz Signed-off-by: fogtape Assisted-by: Codex --- - test/parallel/parallel.status | 1713 +++++++++++++++++ + test/parallel/parallel.status | 1718 +++++++++++++++++ test/parallel/test-bench-stream.js | 3 +- test/parallel/test-common-wpt-inspect.js | 10 +- test/parallel/test-crypto-x509.js | 55 +- @@ -66,13 +66,13 @@ Assisted-by: Codex test/sequential/test-async-wrap-getasyncid.js | 15 +- test/sequential/test-fs-watch.js | 4 + ...test-vm-timeout-escape-promise-module-2.js | 6 +- - 47 files changed, 2144 insertions(+), 88 deletions(-) + 47 files changed, 2149 insertions(+), 88 deletions(-) diff --git a/test/parallel/parallel.status b/test/parallel/parallel.status -index 0fe217d4e4..45d53a6a60 100644 +index 0fe217d4e4..d330d34400 100644 --- a/test/parallel/parallel.status +++ b/test/parallel/parallel.status -@@ -138,6 +138,1719 @@ test-tls-set-default-ca-certificates-append-fetch: SKIP +@@ -138,6 +138,1724 @@ test-tls-set-default-ca-certificates-append-fetch: SKIP test-tls-set-default-ca-certificates-reset-fetch: SKIP test-inspector-invalid-protocol: SKIP @@ -210,6 +210,9 @@ index 0fe217d4e4..45d53a6a60 100644 +test-common-expect-warning: SKIP +test-coverage-with-inspector-disabled: SKIP +test-crypto-fips: SKIP ++# Node 26.12 full-suite triage: switching the FIPS provider at runtime does not ++# finish inside the suite timeout on a mobile device, like test-crypto-fips. ++test-webcrypto-fips-refresh: SKIP +test-crypto-secure-heap: SKIP +test-cwd-enoent*: SKIP +test-debug-usage: SKIP @@ -991,6 +994,8 @@ index 0fe217d4e4..45d53a6a60 100644 +test-common-expect-warning: SKIP +test-coverage-with-inspector-disabled: SKIP +test-crypto-fips: SKIP ++# Node 26.12 full-suite triage: same runtime FIPS provider switch as Android. ++test-webcrypto-fips-refresh: SKIP +test-crypto-no-algorithm: SKIP +test-crypto-secure-heap: SKIP +test-cwd-enoent*: SKIP