diff --git a/.github/workflows/build-tarball.yml b/.github/workflows/build-tarball.yml index 1e1febe22c7f..ab0698e11b74 100644 --- a/.github/workflows/build-tarball.yml +++ b/.github/workflows/build-tarball.yml @@ -120,7 +120,7 @@ jobs: if: github.base_ref == 'main' || github.ref_name == 'main' uses: Mozilla-Actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11 with: - version: v0.16.0 + version: v0.17.0 - name: Download tarball uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: diff --git a/.github/workflows/commit-queue.yml b/.github/workflows/commit-queue.yml index 7af712268711..dff69b18564b 100644 --- a/.github/workflows/commit-queue.yml +++ b/.github/workflows/commit-queue.yml @@ -22,51 +22,47 @@ permissions: contents: read jobs: - get_mergeable_prs: + get_candidate_prs: permissions: pull-requests: read if: github.repository == 'nodejs/node' runs-on: ubuntu-slim outputs: - numbers: ${{ steps.get_mergeable_prs.outputs.numbers }} + candidates: ${{ steps.get_candidate_prs.outputs.candidates }} steps: - - name: Get Pull Requests - id: get_mergeable_prs + - name: Get Pull Request Candidates + id: get_candidate_prs run: | - prs=$(gh pr list \ + list_prs() { + gh pr list \ --repo "$GITHUB_REPOSITORY" \ --base "$GITHUB_REF_NAME" \ --label 'commit-queue' \ + "$@" \ --json 'number' \ - --search "created:<=$(date --date="2 days ago" +"%Y-%m-%dT%H:%M:%S%z") -label:blocked" \ -t '{{ range . }}{{ .number }} {{ end }}' \ - --limit 100) - fast_track_prs=$(gh pr list \ - --repo "$GITHUB_REPOSITORY" \ - --base "$GITHUB_REF_NAME" \ - --label 'commit-queue' \ + --limit 100 + } + aged_prs=$(list_prs \ + --search "created:<=$(date --date="2 days ago" +"%Y-%m-%dT%H:%M:%S%z") -label:blocked") + fast_track_prs=$(list_prs \ --label 'fast-track' \ - --search "-label:blocked" \ - --json 'number' \ - -t '{{ range . }}{{ .number }} {{ end }}' \ - --limit 100) - numbers=$(echo $prs' '$fast_track_prs | jq -r -s 'unique | join(" ")') - echo "numbers=$numbers" >> "$GITHUB_OUTPUT" + --search "-label:blocked") + candidates=$(printf '%s %s\n' "$fast_track_prs" "$aged_prs" | + jq -r -s 'reduce .[] as $pr ([]; if index($pr) then . else . + [$pr] end) | join(" ")') + echo "candidates=$candidates" >> "$GITHUB_OUTPUT" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} commitQueue: - needs: get_mergeable_prs - if: needs.get_mergeable_prs.outputs.numbers != '' + needs: get_candidate_prs + if: needs.get_candidate_prs.outputs.candidates != '' + permissions: + checks: read + contents: read + pull-requests: read + statuses: read runs-on: ubuntu-slim steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - # A personal token is required because pushing with GITHUB_TOKEN will - # prevent commits from running CI after they land. It needs - # to be set here because `checkout` configures GitHub authentication - # for push as well. - token: ${{ secrets.GH_USER_TOKEN }} - # Install dependencies - name: Install Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 @@ -81,19 +77,101 @@ jobs: - name: Configure @node-core/utils run: | - ncu-config set branch "${GITHUB_REF_NAME}" - ncu-config set upstream origin - ncu-config set username "$USERNAME" - ncu-config set token "$GITHUB_TOKEN" - ncu-config set jenkins_token "$JENKINS_TOKEN" - ncu-config set repo "${REPOSITORY}" - ncu-config set owner "${GITHUB_REPOSITORY_OWNER}" + # Keep the config outside the workspace so checkout does not remove it. + ncu-config --global set branch "${GITHUB_REF_NAME}" + ncu-config --global set upstream origin + ncu-config --global set username "$USERNAME" + ncu-config --global set token "$GH_TOKEN" + ncu-config --global set jenkins_token "$JENKINS_TOKEN" + ncu-config --global set repo "${REPOSITORY}" + ncu-config --global set owner "${GITHUB_REPOSITORY_OWNER}" env: USERNAME: ${{ secrets.JENKINS_USER }} - GITHUB_TOKEN: ${{ secrets.GH_USER_TOKEN }} + GH_TOKEN: ${{ github.token }} JENKINS_TOKEN: ${{ secrets.JENKINS_TOKEN }} + - name: Filter Pull Requests + id: get_mergeable_prs + run: | + readme="${RUNNER_TEMP}/README.md" + curl -fsSLo "$readme" "https://github.com/${GITHUB_REPOSITORY}/raw/${GITHUB_SHA}/README.md" + + numbers= + # shellcheck disable=SC2086 + for pr in $CANDIDATES; do + metadata="${RUNNER_TEMP}/metadata-${pr}.json" + output="${RUNNER_TEMP}/metadata-${pr}.txt" + if git node metadata "$pr" \ + --owner "$GITHUB_REPOSITORY_OWNER" \ + --repo "$REPOSITORY" \ + --readme "$readme" \ + --json > "$metadata" 2> "$output"; then + metadata_status=0 + else + metadata_status=$? + fi + + if [ -s "$output" ]; then + cat "$output" + fi + + case "$metadata_status" in + 0|2[0-9]|4[0-9]) ;; + *) + echo "git node metadata failed for pr ${pr} with exit code ${metadata_status}" + exit 1 + ;; + esac + + metadata_exit_code=$(jq -r '.exitCode' "$metadata") || { + echo "failed to parse metadata JSON for pr ${pr}" + exit 1 + } + if [ "$metadata_exit_code" != "$metadata_status" ]; then + echo "metadata JSON exitCode mismatch for pr ${pr}" + exit 1 + fi + metadata_reason_codes=$(jq -r '.reasonCodes | join(", ")' "$metadata") || { + echo "failed to parse metadata reason codes for pr ${pr}" + exit 1 + } + + if [ "$metadata_status" -eq 0 ]; then + echo "pr ${pr} is ready for the commit queue" + numbers="$numbers $pr" + continue + fi + + if [ "$metadata_status" -ge 20 ] && [ "$metadata_status" -le 29 ]; then + echo "pr ${pr} skipped, not ready to land" + echo "reason codes: ${metadata_reason_codes}" + continue + fi + + echo "pr ${pr} will be handled by the commit queue" + echo "reason codes: ${metadata_reason_codes}" + numbers="$numbers $pr" + done + + numbers=$(echo "$numbers" | xargs) + echo "numbers=$numbers" >> "$GITHUB_OUTPUT" + env: + CANDIDATES: ${{ needs.get_candidate_prs.outputs.candidates }} + GH_TOKEN: ${{ github.token }} + + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + if: steps.get_mergeable_prs.outputs.numbers != '' + with: + # A personal token is required because pushing with GITHUB_TOKEN will + # prevent commits from running CI after they land. It needs + # to be set here because `checkout` configures GitHub authentication + # for push as well. + token: ${{ secrets.GH_USER_TOKEN }} + - name: Start the Commit Queue - run: ./tools/actions/commit-queue.sh "${GITHUB_REPOSITORY_OWNER}" "${REPOSITORY}" ${{ needs.get_mergeable_prs.outputs.numbers }} + if: steps.get_mergeable_prs.outputs.numbers != '' + run: | + ncu-config set token "$GH_TOKEN" + ./tools/actions/commit-queue.sh "${GITHUB_REPOSITORY_OWNER}" "${REPOSITORY}" ${{ steps.get_mergeable_prs.outputs.numbers }} env: - GITHUB_TOKEN: ${{ secrets.GH_USER_TOKEN }} + GH_TOKEN: ${{ secrets.GH_USER_TOKEN }} diff --git a/.github/workflows/coverage-linux-without-intl.yml b/.github/workflows/coverage-linux-without-intl.yml index 1519ef6592e8..92c9f3b88217 100644 --- a/.github/workflows/coverage-linux-without-intl.yml +++ b/.github/workflows/coverage-linux-without-intl.yml @@ -66,7 +66,7 @@ jobs: if: github.base_ref == 'main' || github.ref_name == 'main' uses: Mozilla-Actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11 with: - version: v0.16.0 + version: v0.17.0 - name: Install gcovr run: pip install gcovr==7.2 - name: Configure diff --git a/.github/workflows/coverage-linux.yml b/.github/workflows/coverage-linux.yml index e4a3c334c8cd..e97b759bc6a4 100644 --- a/.github/workflows/coverage-linux.yml +++ b/.github/workflows/coverage-linux.yml @@ -66,7 +66,7 @@ jobs: if: github.base_ref == 'main' || github.ref_name == 'main' uses: Mozilla-Actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11 with: - version: v0.16.0 + version: v0.17.0 - name: Install gcovr run: pip install gcovr==7.2 - name: Configure diff --git a/.github/workflows/major-release.yml b/.github/workflows/major-release.yml index b65917f89e74..1b15a00c8df1 100644 --- a/.github/workflows/major-release.yml +++ b/.github/workflows/major-release.yml @@ -2,7 +2,7 @@ name: Major Release on: schedule: - - cron: 0 0 15 2,8 * # runs at midnight UTC every 15 February and 15 August + - cron: 0 0 15 2 * # runs at midnight UTC every 15 February permissions: contents: read diff --git a/.github/workflows/stress-test.yml b/.github/workflows/stress-test.yml index b6fa42137d5e..6f1e75813915 100644 --- a/.github/workflows/stress-test.yml +++ b/.github/workflows/stress-test.yml @@ -78,7 +78,7 @@ jobs: - name: Set up sccache uses: Mozilla-Actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11 with: - version: v0.16.0 + version: v0.17.0 # This is needed due to https://github.com/nodejs/build/issues/3878 - name: Cleanup if: runner.os == 'macOS' diff --git a/.github/workflows/test-internet.yml b/.github/workflows/test-internet.yml index bcb9ff76372f..7052f014b200 100644 --- a/.github/workflows/test-internet.yml +++ b/.github/workflows/test-internet.yml @@ -63,7 +63,7 @@ jobs: if: github.base_ref == 'main' || github.ref_name == 'main' uses: Mozilla-Actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11 with: - version: v0.16.0 + version: v0.17.0 - name: Build run: make build-ci -j4 V=1 CONFIG_FLAGS="--error-on-warn" - name: Test Internet diff --git a/.github/workflows/test-linux-quic.yml b/.github/workflows/test-linux-quic.yml index 38d2ef9b8407..e1a05cf91859 100644 --- a/.github/workflows/test-linux-quic.yml +++ b/.github/workflows/test-linux-quic.yml @@ -68,7 +68,7 @@ jobs: if: github.base_ref == 'main' || github.ref_name == 'main' uses: Mozilla-Actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11 with: - version: v0.16.0 + version: v0.17.0 - name: Build working-directory: node run: make build-ci -j4 V=1 CONFIG_FLAGS="--error-on-warn --v8-enable-temporal-support --experimental-quic" diff --git a/.github/workflows/test-linux.yml b/.github/workflows/test-linux.yml index c38bae0693fd..40762503f685 100644 --- a/.github/workflows/test-linux.yml +++ b/.github/workflows/test-linux.yml @@ -79,7 +79,7 @@ jobs: if: github.base_ref == 'main' || github.ref_name == 'main' uses: Mozilla-Actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11 with: - version: v0.16.0 + version: v0.17.0 - name: Build working-directory: node run: make build-ci -j4 V=1 CONFIG_FLAGS="--error-on-warn --v8-enable-temporal-support" diff --git a/.github/workflows/test-macos.yml b/.github/workflows/test-macos.yml index 173f6758ad71..e87e83505d6d 100644 --- a/.github/workflows/test-macos.yml +++ b/.github/workflows/test-macos.yml @@ -102,7 +102,7 @@ jobs: if: github.base_ref == 'main' || github.ref_name == 'main' uses: Mozilla-Actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11 with: - version: v0.16.0 + version: v0.17.0 # The `npm ci` for this step fails a lot as part of the Test step. Run it # now so that we don't have to wait 2 hours for the Build step to pass # first before that failure happens. (And if there's something about diff --git a/.github/workflows/test-shared.yml b/.github/workflows/test-shared.yml index a8f2ddc27831..284372317359 100644 --- a/.github/workflows/test-shared.yml +++ b/.github/workflows/test-shared.yml @@ -246,7 +246,7 @@ jobs: with: runner: ubuntu-24.04-arm v8-nar: ${{ needs.build-aarch64-linux-v8.outputs.local-cache && 'libv8-aarch64-linux.nar' }} - pkcs11-store-test: ${{ matrix.openssl.attr == 'openssl_3_5' }} + pkcs11-store-test: ${{ matrix.openssl.attr == 'openssl' }} # Override just the `openssl` attr of the default shared-lib set with # the matrix-selected nixpkgs attribute (e.g. `openssl_3_6`). All # other shared libs (brotli, cares, libuv, …) keep their defaults. diff --git a/.github/workflows/tools.yml b/.github/workflows/tools.yml index 0008b0478f55..e80b65315a4a 100644 --- a/.github/workflows/tools.yml +++ b/.github/workflows/tools.yml @@ -35,6 +35,7 @@ on: - nghttp2 - nghttp3 - ngtcp2 + - perfetto - postject - root-certificates - simdjson @@ -237,6 +238,14 @@ jobs: cat temp-output tail -n1 temp-output | grep "NEW_VERSION=" >> "$GITHUB_ENV" || true rm temp-output + - id: perfetto + subsystem: deps + label: dependencies + run: | + ./tools/dep_updaters/update-perfetto.sh > temp-output + cat temp-output + tail -n1 temp-output | grep "NEW_VERSION=" >> "$GITHUB_ENV" || true + rm temp-output - id: postject subsystem: deps,test label: test @@ -336,7 +345,7 @@ jobs: # no-op if the base branch is already up-to-date. with: token: ${{ secrets.GH_USER_TOKEN }} - branch: actions/${{ github.ref_name == 'main' || format('{0}/', github.ref_name) }}tools-update-${{ matrix.id }} # Custom branch *just* for this Action. + branch: actions/${{ github.ref_name != 'main' && format('{0}/', github.ref_name) || '' }}tools-update-${{ matrix.id }} # Custom branch *just* for this Action. delete-branch: true commit-message: ${{ env.COMMIT_MSG }} labels: ${{ matrix.label }} diff --git a/.mailmap b/.mailmap index 0860e8e01478..6cdb3bc4f739 100644 --- a/.mailmap +++ b/.mailmap @@ -55,6 +55,7 @@ Ashok Suthar Ashutosh Kumar Singh Atsuo Fukaya Austin Kelleher +Aviv Keller Azard <330815461@qq.com> Ben Lugavere Ben Noordhuis diff --git a/BUILDING.md b/BUILDING.md index 02ea54c2f8ac..e477d46863f4 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -1032,11 +1032,11 @@ as `deps/icu` (You'll have: `deps/icu/source/...`) ### Configure OpenSSL appname Node.js can use an OpenSSL configuration file by specifying the environment -variable `OPENSSL_CONF`, or using the command line option `--openssl-conf`, and -if none of those are specified will default to reading the default OpenSSL -configuration file `openssl.cnf`. Node.js will only read a section that is by -default named `nodejs_conf`, but this name can be overridden using the following -configure option: +variable `OPENSSL_CONF`, or using the command line option `--openssl-config`, +which takes precedence. If neither is specified, Node.js defaults to reading the +default OpenSSL configuration file `openssl.cnf`. Node.js will only read a +section that is by default named `nodejs_conf`, but this name can be overridden +using the following configure option: ```bash ./configure --openssl-conf-name= @@ -1048,6 +1048,8 @@ Node.js supports FIPS when statically or dynamically linked with OpenSSL 3 via [OpenSSL's provider model](https://docs.openssl.org/3.0/man7/crypto/#OPENSSL-PROVIDERS). It is not necessary to rebuild Node.js to enable support for FIPS. +When using OpenSSL 1.1.1, Node.js must be built against a FIPS-capable OpenSSL. + See [FIPS mode](doc/api/crypto.md#fips-mode) for more information on how to enable FIPS support in Node.js. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b47b9868461b..90a8a1d50f4f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,6 +25,7 @@ works. * [Issues](#issues) * [Pull Requests](#pull-requests) * [Automation and bots](#automation-and-bots) +* [AI Use Policy and Guidelines](#ai-use-policy-and-guidelines) * [Developer's Certificate of Origin 1.1](#developers-certificate-of-origin-11) ## [Code of Conduct](./doc/contributing/code-of-conduct.md) @@ -66,6 +67,15 @@ by an automation that was not authorized by Node.js collaborators are subject to immediate moderation enforcement on the automation and owner without notice. +## [AI Use Policy and Guidelines](./doc/contributing/ai-guidelines.md) + +Node.js requires contributors to understand and take full responsibility for +every change they propose. Pull requests containing AI-generated code the +contributor has not personally understood, tested, and verified will likely be closed +without review. + +See [details on our AI use policy and guidelines](./doc/contributing/ai-guidelines.md). + ## Developer's Certificate of Origin 1.1 ```text diff --git a/benchmark/ffi/get-function.js b/benchmark/ffi/get-function.js new file mode 100644 index 000000000000..3c1e2e974ce6 --- /dev/null +++ b/benchmark/ffi/get-function.js @@ -0,0 +1,48 @@ +'use strict'; + +// Measures symbol resolution rather than call throughput. Creating a callable +// for a fast-eligible signature emits a native trampoline, so this benchmark +// covers the trampoline allocation path that the call benchmarks never reach. +// +// The `fast` variant is eligible for a generated trampoline; `slow` exceeds the +// x86_64 register budget and falls back, so it resolves without allocating one. +// Comparing the two isolates trampoline creation cost from the rest of symbol +// resolution. + +const common = require('../common.js'); +const { DynamicLibrary } = require('node:ffi'); +const { libraryPath, ensureFixtureLibrary } = require('./common.js'); + +const bench = common.createBenchmark(main, { + signature: ['fast', 'slow'], + n: [1e3], +}, { + flags: ['--experimental-ffi'], +}); + +ensureFixtureLibrary(); + +const signatures = { + fast: { name: 'add_i32', return: 'i32', arguments: ['i32', 'i32'] }, + slow: { + name: 'sum_8_i32', + return: 'i32', + arguments: ['i32', 'i32', 'i32', 'i32', 'i32', 'i32', 'i32', 'i32'], + }, +}; + +function main({ n, signature }) { + const { name, ...definition } = signatures[signature]; + const lib = new DynamicLibrary(libraryPath); + + // Warm up one-time initialization (libffi setup, executable memory probe) so + // it is not attributed to the measured resolutions. + lib.getFunction(name, definition); + + bench.start(); + for (let i = 0; i < n; ++i) + lib.getFunction(name, definition); + bench.end(n); + + lib.close(); +} diff --git a/benchmark/net/net-blocklist.js b/benchmark/net/net-blocklist.js new file mode 100644 index 000000000000..9c293682ff61 --- /dev/null +++ b/benchmark/net/net-blocklist.js @@ -0,0 +1,146 @@ +'use strict'; + +const common = require('../common.js'); +const { BlockList, SocketAddress } = require('net'); + +const hasAddAddresses = typeof BlockList.prototype.addAddresses === 'function'; + +const operations = ['check', 'checkWithSocketAddress', 'addAddress']; +if (hasAddAddresses) { + operations.push('addAddresses'); +} + +const bench = common.createBenchmark(main, { + n: [1e6], + ruleCount: [10, 100, 1000, 10000], + ruleType: ['address', 'subnet', 'mixed'], + checkResult: ['hit', 'miss'], + operation: operations, +}, { + combinationFilter({ operation, ruleCount, ruleType }) { + // addAddress and addAddresses only need address rules, not subnets. + if ((operation === 'addAddress' || operation === 'addAddresses') && + ruleType !== 'address') { + return false; + } + return true; + }, +}); + +function generateIPv4(index) { + return `${(index >>> 24) & 0xff}.${(index >>> 16) & 0xff}.` + + `${(index >>> 8) & 0xff}.${index & 0xff}`; +} + +function buildBlockList(ruleCount, ruleType) { + const blockList = new BlockList(); + + if (ruleType === 'address' || ruleType === 'mixed') { + const addressCount = ruleType === 'mixed' ? + Math.floor(ruleCount / 2) : ruleCount; + const addresses = []; + for (let i = 0; i < addressCount; i++) { + // Start from 10.0.0.1 to avoid 0.0.0.0 + addresses.push(generateIPv4(0x0a000001 + i)); + } + if (hasAddAddresses) { + blockList.addAddresses(addresses); + } else { + for (const addr of addresses) { + blockList.addAddress(addr); + } + } + } + + if (ruleType === 'subnet' || ruleType === 'mixed') { + const subnetCount = ruleType === 'mixed' ? + Math.floor(ruleCount / 2) : ruleCount; + for (let i = 0; i < subnetCount; i++) { + // Use distinct /24 subnets: 172.i.j.0/24 + const second = (i >>> 8) & 0xff; + const third = i & 0xff; + blockList.addSubnet(`172.${second}.${third}.0`, 24); + } + } + + return blockList; +} + +function main({ n, ruleCount, ruleType, checkResult, operation }) { + if (operation === 'check') { + benchCheck(n, ruleCount, ruleType, checkResult); + } else if (operation === 'checkWithSocketAddress') { + benchCheckWithSocketAddress(n, ruleCount, ruleType, checkResult); + } else if (operation === 'addAddress') { + benchAddAddress(n, ruleCount); + } else if (operation === 'addAddresses') { + benchAddAddresses(n, ruleCount); + } +} + +// Benchmark check() with string addresses (the common JS API path). +function benchCheck(n, ruleCount, ruleType, checkResult) { + const blockList = buildBlockList(ruleCount, ruleType); + + // For 'hit', use an address that's in the list. + // For 'miss', use an address that's not in the list. + const address = checkResult === 'hit' ? '10.0.0.1' : '192.168.255.255'; + + bench.start(); + for (let i = 0; i < n; i++) { + blockList.check(address); + } + bench.end(n); +} + +// Benchmark check() with pre-created SocketAddress objects +// (avoids measuring SocketAddress construction overhead). +function benchCheckWithSocketAddress(n, ruleCount, ruleType, checkResult) { + const blockList = buildBlockList(ruleCount, ruleType); + + const address = checkResult === 'hit' ? '10.0.0.1' : '192.168.255.255'; + const sa = new SocketAddress({ address }); + + bench.start(); + for (let i = 0; i < n; i++) { + blockList.check(sa); + } + bench.end(n); +} + +// Benchmark single addAddress() calls (one lock acquire per call). +function benchAddAddress(n, ruleCount) { + // Scale n down for large rule counts to keep runtime reasonable. + const iterations = Math.min(n, ruleCount * 100); + + const addresses = []; + for (let i = 0; i < ruleCount; i++) { + addresses.push(generateIPv4(0x0a000001 + i)); + } + + bench.start(); + for (let i = 0; i < iterations; i++) { + const blockList = new BlockList(); + for (let j = 0; j < addresses.length; j++) { + blockList.addAddress(addresses[j]); + } + } + bench.end(iterations); +} + +// Benchmark batch addAddresses() (one lock acquire per batch). +function benchAddAddresses(n, ruleCount) { + const iterations = Math.min(n, ruleCount * 100); + + const addresses = []; + for (let i = 0; i < ruleCount; i++) { + addresses.push(generateIPv4(0x0a000001 + i)); + } + + bench.start(); + for (let i = 0; i < iterations; i++) { + const blockList = new BlockList(); + blockList.addAddresses(addresses); + } + bench.end(iterations); +} diff --git a/benchmark/test_runner/hooks.js b/benchmark/test_runner/hooks.js new file mode 100644 index 000000000000..dc73ff4fb1e1 --- /dev/null +++ b/benchmark/test_runner/hooks.js @@ -0,0 +1,51 @@ +'use strict'; + +const common = require('../common'); +const { finished } = require('node:stream/promises'); +const reporter = require('../fixtures/empty-test-reporter'); +const { + after, + afterEach, + before, + beforeEach, + describe, + it, +} = require('node:test'); + +const bench = common.createBenchmark(main, { + n: [1000], + hook: ['before', 'after', 'beforeEach', 'afterEach'], +}, { + // We don't want to test the reporter here. + flags: ['--test-reporter=./benchmark/fixtures/empty-test-reporter.js'], +}); + +const hookList = { + before: before, + after: after, + beforeEach: beforeEach, + afterEach: afterEach, +}; + +const noop = () => {}; + +function run(loopAmount, hookFn) { + for (let i = 0; i < loopAmount; i++) { + describe(`${i}`, () => { + hookFn(noop); + it(`${i}`, noop); + }); + } + + return finished(reporter); +} + +function main(params) { + const hookFn = hookList[params.hook]; + + bench.start(); + + run(params.n, hookFn).then(() => { + bench.end(params.n); + }); +} diff --git a/benchmark/test_runner/test-options.js b/benchmark/test_runner/test-options.js new file mode 100644 index 000000000000..1d608c1f9ccb --- /dev/null +++ b/benchmark/test_runner/test-options.js @@ -0,0 +1,114 @@ +'use strict'; + +const common = require('../common'); +const { finished } = require('node:stream/promises'); +const reporter = require('../fixtures/empty-test-reporter'); +const { it } = require('node:test'); + +const bench = common.createBenchmark(main, { + n: [10000], + option: [ + 'none', + 'skip', + 'skip-with-message', + 'skip-method', + 'skip-method-with-message', + 'todo', + 'todo-with-message', + 'todo-method', + 'todo-method-with-message', + ], +}, { + // We don't want to test the reporter here. + flags: ['--test-reporter=./benchmark/fixtures/empty-test-reporter.js'], +}); + +const noop = () => {}; + +const allTests = { + 'none': (loopAmount) => { + for (let i = 0; i < loopAmount; i++) { + it(`${i}`, noop); + } + + return finished(reporter); + }, + 'skip': (loopAmount) => { + for (let i = 0; i < loopAmount; i++) { + it(`${i}`, { skip: true }, () => { + throw new Error('This test should not run.'); + }); + } + + return finished(reporter); + }, + 'skip-with-message': (loopAmount) => { + for (let i = 0; i < loopAmount; i++) { + it(`${i}`, { skip: 'skip reason' }, () => { + throw new Error('This test should not run.'); + }); + } + + return finished(reporter); + }, + 'skip-method': (loopAmount) => { + for (let i = 0; i < loopAmount; i++) { + it(`${i}`, (t) => { + t.skip(); + }); + } + + return finished(reporter); + }, + 'skip-method-with-message': (loopAmount) => { + for (let i = 0; i < loopAmount; i++) { + it(`${i}`, (t) => { + t.skip('skip reason'); + }); + } + + return finished(reporter); + }, + 'todo': (loopAmount) => { + for (let i = 0; i < loopAmount; i++) { + it(`${i}`, { todo: true }, noop); + } + + return finished(reporter); + }, + 'todo-with-message': (loopAmount) => { + for (let i = 0; i < loopAmount; i++) { + it(`${i}`, { todo: 'todo reason' }, noop); + } + + return finished(reporter); + }, + 'todo-method': (loopAmount) => { + for (let i = 0; i < loopAmount; i++) { + it(`${i}`, (t) => { + t.todo(); + }); + } + + return finished(reporter); + }, + 'todo-method-with-message': (loopAmount) => { + for (let i = 0; i < loopAmount; i++) { + it(`${i}`, (t) => { + t.todo('todo reason'); + }); + } + + return finished(reporter); + }, +}; + +function main({ n, option }) { + const runOption = allTests[option]; + + bench.start(); + + runOption(n).then(() => { + bench.end(n); + }); +} diff --git a/configure.py b/configure.py index 20bf5b7d101f..b663ba85f58e 100755 --- a/configure.py +++ b/configure.py @@ -1101,7 +1101,7 @@ action='store_true', dest='enable_static', default=None, - help='build as static library') + help=argparse.SUPPRESS) # Deprecated parser.add_argument('--no-browser-globals', action='store_true', @@ -1517,7 +1517,7 @@ def get_openssl_version(o): return version_number - except (OSError, ValueError, subprocess.SubprocessError) as e: + except (OSError, TypeError, ValueError, subprocess.SubprocessError) as e: warn(f'Failed to determine OpenSSL version from header: {e}') return 0 @@ -2075,9 +2075,6 @@ def configure_node(o): if options.v8_options: o['variables']['node_v8_options'] = options.v8_options.replace('"', '\\"') - if options.enable_static: - o['variables']['node_target_type'] = 'static_library' - o['variables']['node_debug_lib'] = b(options.node_debug_lib) if options.debug_nghttp2: @@ -2120,10 +2117,13 @@ def configure_node(o): else: o['variables']['coverage'] = 'false' + if options.enable_static and options.shared: + error('--enable-static must not be set with --shared') + if options.enable_static: + warn('--enable-static is deprecated and libnode.a is always produced') + if options.shared: o['variables']['node_target_type'] = 'shared_library' - elif options.enable_static: - o['variables']['node_target_type'] = 'static_library' else: o['variables']['node_target_type'] = 'executable' diff --git a/deps/googletest/include/gtest/gtest-matchers.h b/deps/googletest/include/gtest/gtest-matchers.h index d7bdd047f1a6..b5950425cc69 100644 --- a/deps/googletest/include/gtest/gtest-matchers.h +++ b/deps/googletest/include/gtest/gtest-matchers.h @@ -45,6 +45,7 @@ #include #include #include +#include #include #include "gtest/gtest-printers.h" @@ -543,9 +544,8 @@ Matcher : public internal::MatcherBase { Matcher(const char* s); // NOLINT }; -#if GTEST_INTERNAL_HAS_STRING_VIEW // The following two specializations allow the user to write str -// instead of Eq(str) and "foo" instead of Eq("foo") when a absl::string_view +// instead of Eq(str) and "foo" instead of Eq("foo") when a std::string_view // matcher is expected. template <> class GTEST_API_ [[nodiscard]] Matcher @@ -569,7 +569,7 @@ class GTEST_API_ [[nodiscard]] Matcher // Allows the user to write "foo" instead of Eq("foo") sometimes. Matcher(const char* s); // NOLINT - // Allows the user to pass absl::string_views or std::string_views directly. + // Allows the user to pass std::string_views directly. Matcher(internal::StringView s); // NOLINT }; @@ -596,10 +596,9 @@ class GTEST_API_ [[nodiscard]] Matcher // Allows the user to write "foo" instead of Eq("foo") sometimes. Matcher(const char* s); // NOLINT - // Allows the user to pass absl::string_views or std::string_views directly. + // Allows the user to pass std::string_views directly. Matcher(internal::StringView s); // NOLINT }; -#endif // GTEST_INTERNAL_HAS_STRING_VIEW // Prints a matcher in a human-readable format. template @@ -812,9 +811,26 @@ class [[nodiscard]] ImplicitCastEqMatcher { StoredRhs stored_rhs_; }; -template >> -using StringLike = T; +// Dummy function (never defined) whose return type evaluates to std::string if +// the given type is a string-like type that can be converted to std::string, +// either directly or through an intermediate std::string_view. +template +extern std::enable_if_t, std::string> +ResolveAsString(const void* /* preferred */); + +#if GTEST_HAS_STD_WSTRING +// Same as above, but for std::wstring. In cases where both conversions are +// possible, this overload takes lower priority. +template +extern std::enable_if_t, std::wstring> +ResolveAsString(... /* fallback */); +#endif + +// Evaluates to the std::basic_string type that the given string-like type can +// be converted to. Prefers std::string over std::wstring if both are possible. +// Fails in a SFINAE-friendly way if no conversion was viable. +template +using StringType = decltype(ResolveAsString(nullptr)); // Implements polymorphic matchers MatchesRegex(regex) and // ContainsRegex(regex), which can be used as a Matcher as long as @@ -824,12 +840,10 @@ class [[nodiscard]] MatchesRegexMatcher { MatchesRegexMatcher(const RE* regex, bool full_match) : regex_(regex), full_match_(full_match) {} -#if GTEST_INTERNAL_HAS_STRING_VIEW bool MatchAndExplain(const internal::StringView& s, MatchResultListener* listener) const { return MatchAndExplain(std::string(s), listener); } -#endif // GTEST_INTERNAL_HAS_STRING_VIEW // Accepts pointer types, particularly: // const char* @@ -844,7 +858,7 @@ class [[nodiscard]] MatchesRegexMatcher { // Matches anything that can convert to std::string. // // This is a template, not just a plain function with const std::string&, - // because absl::string_view has some interfering non-explicit constructors. + // because std::string_view has some interfering non-explicit constructors. template bool MatchAndExplain(const MatcheeStringType& s, MatchResultListener* /* listener */) const { @@ -877,9 +891,10 @@ inline PolymorphicMatcher MatchesRegex( return MakePolymorphicMatcher(internal::MatchesRegexMatcher(regex, true)); } template -PolymorphicMatcher MatchesRegex( - const internal::StringLike& regex) { - return MatchesRegex(new internal::RE(std::string(regex))); +std::enable_if_t>, + PolymorphicMatcher> +MatchesRegex(const T& regex) { + return MatchesRegex(new internal::RE(internal::StringType(regex))); } // Matches a string that contains regular expression 'regex'. @@ -889,9 +904,10 @@ inline PolymorphicMatcher ContainsRegex( return MakePolymorphicMatcher(internal::MatchesRegexMatcher(regex, false)); } template -PolymorphicMatcher ContainsRegex( - const internal::StringLike& regex) { - return ContainsRegex(new internal::RE(std::string(regex))); +std::enable_if_t>, + PolymorphicMatcher> +ContainsRegex(const T& regex) { + return ContainsRegex(new internal::RE(internal::StringType(regex))); } // Creates a polymorphic matcher that matches anything equal to x. diff --git a/deps/googletest/include/gtest/gtest-printers.h b/deps/googletest/include/gtest/gtest-printers.h index fc0913ff0094..315ce0a51016 100644 --- a/deps/googletest/include/gtest/gtest-printers.h +++ b/deps/googletest/include/gtest/gtest-printers.h @@ -291,11 +291,9 @@ struct ConvertibleToIntegerPrinter { }; struct ConvertibleToStringViewPrinter { -#if GTEST_INTERNAL_HAS_STRING_VIEW static void PrintValue(internal::StringView value, ::std::ostream* os) { internal::UniversalPrint(value, os); } -#endif }; #ifdef GTEST_HAS_ABSL @@ -703,12 +701,12 @@ void PrintRawArrayTo(const T a[], size_t count, ::std::ostream* os) { } } -// Overloads for ::std::string and ::std::string_view -GTEST_API_ void PrintStringTo(::std::string_view s, ::std::ostream* os); +// Overloads for ::std::string and std::string_view +GTEST_API_ void PrintStringTo(std::string_view s, ::std::ostream* os); inline void PrintTo(const ::std::string& s, ::std::ostream* os) { PrintStringTo(s, os); } -inline void PrintTo(::std::string_view s, ::std::ostream* os) { +inline void PrintTo(std::string_view s, ::std::ostream* os) { PrintStringTo(s, os); } @@ -752,16 +750,14 @@ inline void PrintTo(::std::wstring_view s, ::std::ostream* os) { } #endif // GTEST_HAS_STD_WSTRING -#if GTEST_INTERNAL_HAS_STRING_VIEW // Overload for internal::StringView. Needed for build configurations where // internal::StringView is an alias for absl::string_view, but absl::string_view // is a distinct type from std::string_view. template , int> = 0> + std::enable_if_t, int> = 0> inline void PrintTo(internal::StringView sp, ::std::ostream* os) { PrintStringTo(sp, os); } -#endif // GTEST_INTERNAL_HAS_STRING_VIEW inline void PrintTo(std::nullptr_t, ::std::ostream* os) { *os << "(nullptr)"; } diff --git a/deps/googletest/include/gtest/internal/gtest-death-test-internal.h b/deps/googletest/include/gtest/internal/gtest-death-test-internal.h index f88e2049c249..f0f93e520b7b 100644 --- a/deps/googletest/include/gtest/internal/gtest-death-test-internal.h +++ b/deps/googletest/include/gtest/internal/gtest-death-test-internal.h @@ -43,6 +43,7 @@ #include #include +#include #include "gtest/gtest-matchers.h" #include "gtest/internal/gtest-internal.h" @@ -63,6 +64,10 @@ inline Matcher MakeDeathTestMatcher( ::testing::internal::RE regex) { return ContainsRegex(regex.pattern()); } +inline Matcher MakeDeathTestMatcher( + std::string_view regex) { + return ContainsRegex(regex); +} inline Matcher MakeDeathTestMatcher(const char* regex) { return ContainsRegex(regex); } diff --git a/deps/googletest/include/gtest/internal/gtest-internal.h b/deps/googletest/include/gtest/internal/gtest-internal.h index 2b048c5dc098..55e9966720bf 100644 --- a/deps/googletest/include/gtest/internal/gtest-internal.h +++ b/deps/googletest/include/gtest/internal/gtest-internal.h @@ -1451,13 +1451,13 @@ class [[nodiscard]] NeverThrown { // Implements Boolean test assertions such as EXPECT_TRUE. expression can be // either a boolean expression or an AssertionResult. text is a textual // representation of expression as it was passed into the EXPECT_TRUE. -#define GTEST_TEST_BOOLEAN_(expression, text, actual, expected, fail) \ - GTEST_AMBIGUOUS_ELSE_BLOCKER_ \ - if (::testing::internal::AssertionResultExpectation gtest_are_ = { \ - ::testing::AssertionResult(expression), expected}) \ - ; \ - else \ - fail(::testing::internal::GetBoolAssertionFailureMessage( \ +#define GTEST_TEST_BOOLEAN_(expression, text, actual, expected, fail) \ + GTEST_AMBIGUOUS_ELSE_BLOCKER_ \ + if (const ::testing::internal::AssertionResultExpectation gtest_are_ = { \ + ::testing::AssertionResult(expression), expected}) \ + ; \ + else /* NOLINT */ \ + fail(::testing::internal::GetBoolAssertionFailureMessage( \ gtest_are_.assertion_result, text, #actual, #expected)) #define GTEST_TEST_NO_FATAL_FAILURE_(statement, fail) \ diff --git a/deps/googletest/include/gtest/internal/gtest-port.h b/deps/googletest/include/gtest/internal/gtest-port.h index 31654b09c1dc..92e6591d2cec 100644 --- a/deps/googletest/include/gtest/internal/gtest-port.h +++ b/deps/googletest/include/gtest/internal/gtest-port.h @@ -293,9 +293,10 @@ #include #include #include +// #include // Guarded by GTEST_IS_THREADSAFE below #include #include -// #include // Guarded by GTEST_IS_THREADSAFE below +#include #include #include #include @@ -949,21 +950,21 @@ GTEST_API_ bool IsTrue(bool condition); #ifdef GTEST_USES_RE2 // This is almost `using RE = ::RE2`, except it is copy-constructible, and it -// needs to disambiguate the `std::string`, `absl::string_view`, and `const +// needs to disambiguate the `std::string`, `std::string_view`, and `const // char*` constructors. class GTEST_API_ [[nodiscard]] RE { public: - RE(absl::string_view regex) : regex_(regex) {} // NOLINT - RE(const char* regex) : RE(absl::string_view(regex)) {} // NOLINT - RE(const std::string& regex) : RE(absl::string_view(regex)) {} // NOLINT + RE(std::string_view regex) : regex_(regex) {} // NOLINT + RE(const char* regex) : RE(std::string_view(regex)) {} // NOLINT + RE(const std::string& regex) : RE(std::string_view(regex)) {} // NOLINT RE(const RE& other) : RE(other.pattern()) {} const std::string& pattern() const { return regex_.pattern(); } - static bool FullMatch(absl::string_view str, const RE& re) { + static bool FullMatch(std::string_view str, const RE& re) { return RE2::FullMatch(str, re.regex_); } - static bool PartialMatch(absl::string_view str, const RE& re) { + static bool PartialMatch(std::string_view str, const RE& re) { return RE2::PartialMatch(str, re.regex_); } @@ -2396,7 +2397,6 @@ const char* StringFromGTestEnv(const char* flag, const char* default_val); #ifdef GTEST_HAS_ABSL // Always use absl::string_view for Matcher<> specializations if googletest // is built with absl support. -#define GTEST_INTERNAL_HAS_STRING_VIEW 1 #include "absl/strings/string_view.h" namespace testing { namespace internal { @@ -2404,26 +2404,15 @@ using StringView = ::absl::string_view; } // namespace internal } // namespace testing #else -#if defined(__cpp_lib_string_view) || \ - (GTEST_INTERNAL_HAS_INCLUDE() && \ - GTEST_INTERNAL_CPLUSPLUS_LANG >= 201703L) // Otherwise for C++17 and higher use std::string_view for Matcher<> // specializations. -#define GTEST_INTERNAL_HAS_STRING_VIEW 1 -#include namespace testing { namespace internal { -using StringView = ::std::string_view; +using StringView = std::string_view; } // namespace internal } // namespace testing -// The case where absl is configured NOT to alias std::string_view is not -// supported. -#endif // __cpp_lib_string_view #endif // GTEST_HAS_ABSL - -#ifndef GTEST_INTERNAL_HAS_STRING_VIEW -#define GTEST_INTERNAL_HAS_STRING_VIEW 0 -#endif +#define GTEST_INTERNAL_HAS_STRING_VIEW 1 #if defined(__cpp_lib_three_way_comparison) #define GTEST_INTERNAL_HAS_COMPARE_LIB 1 diff --git a/deps/googletest/src/gtest-matchers.cc b/deps/googletest/src/gtest-matchers.cc index 7e3bcc0cff38..626019e2389f 100644 --- a/deps/googletest/src/gtest-matchers.cc +++ b/deps/googletest/src/gtest-matchers.cc @@ -59,7 +59,6 @@ Matcher::Matcher(const std::string& s) { *this = Eq(s); } // s. Matcher::Matcher(const char* s) { *this = Eq(std::string(s)); } -#if GTEST_INTERNAL_HAS_STRING_VIEW // Constructs a matcher that matches a const StringView& whose value is // equal to s. Matcher::Matcher(const std::string& s) { @@ -93,6 +92,5 @@ Matcher::Matcher(const char* s) { Matcher::Matcher(internal::StringView s) { *this = Eq(std::string(s)); } -#endif // GTEST_INTERNAL_HAS_STRING_VIEW } // namespace testing diff --git a/deps/googletest/src/gtest-printers.cc b/deps/googletest/src/gtest-printers.cc index 6d1de6d9506f..7c0ecc6ad1c9 100644 --- a/deps/googletest/src/gtest-printers.cc +++ b/deps/googletest/src/gtest-printers.cc @@ -515,13 +515,13 @@ bool IsValidUTF8(const char* str, size_t length) { void ConditionalPrintAsText(const char* str, size_t length, ostream* os) { if (!ContainsUnprintableControlCodes(str, length) && IsValidUTF8(str, length)) { - *os << "\n As Text: \"" << ::std::string_view(str, length) << "\""; + *os << "\n As Text: \"" << std::string_view(str, length) << "\""; } } } // anonymous namespace -void PrintStringTo(::std::string_view s, ostream* os) { +void PrintStringTo(std::string_view s, ostream* os) { if (PrintCharsAsStringTo(s.data(), s.size(), os) == kHexEscape) { if (GTEST_FLAG_GET(print_utf8)) { ConditionalPrintAsText(s.data(), s.size(), os); diff --git a/deps/googletest/src/gtest.cc b/deps/googletest/src/gtest.cc index 307ecc6f0b9c..3c855468268f 100644 --- a/deps/googletest/src/gtest.cc +++ b/deps/googletest/src/gtest.cc @@ -6930,7 +6930,7 @@ void ParseGoogleTestFlagsOnly(int* argc, char** argv) { std::vector positional_args; std::vector unrecognized_flags; absl::ParseAbseilFlagsOnly(*argc, argv, positional_args, unrecognized_flags); - absl::flat_hash_set unrecognized; + absl::flat_hash_set unrecognized; for (const auto& flag : unrecognized_flags) { unrecognized.insert(flag.flag_name); } diff --git a/deps/libffi/ChangeLog b/deps/libffi/ChangeLog index 0dde93e6adad..f64d5d5c46ac 100644 --- a/deps/libffi/ChangeLog +++ b/deps/libffi/ChangeLog @@ -1,3 +1,610 @@ +commit 12ffd1f9dc56fcea79d2f742f424301ae668d663 +Author: Anthony Green +Date: Sat Aug 8 18:08:29 2026 -0400 + + README: order 3.8.0 notes by decreasing importance + + Lead with new capabilities (VECTOR types, ffi_call_plan_size, ppc64 + _Complex long double), then correctness fixes by severity, then the + trampoline caching optimization. + + Co-Authored-By: Claude Opus 4.8 (1M context) + +commit 8f2a41d9d89dd8ee2c2438f1e2f9cf04aa9a53d4 +Author: Anthony Green +Date: Sat Aug 8 18:05:33 2026 -0400 + + Release 3.8.0 + + Bump version to 3.8.0, soname to libffi.so.8.5.0 (libtool 13:0:5) for the + new public interfaces added this cycle (FFI_TYPE_VECTOR, ffi_call_plan_size), + date the README history section, and update doc/version.texi. + + Co-Authored-By: Claude Opus 4.8 (1M context) + +commit d956fe177ccfd4bb91ae7cb3ccaa0f8935a76522 +Author: Anthony Green +Date: Sat Aug 8 17:38:40 2026 -0400 + + testsuite: distribute plan_size.c + + The ffi_call_plan_size test added in #1006 was not listed in EXTRA_DIST, + so it would be omitted from release tarballs (it still runs from a git + checkout, where dejagnu globs *.c). Add it alongside the other plan_*.c + tests. + + Co-Authored-By: Claude Opus 4.8 (1M context) + +commit 670a0327b7d1576712a3cad7b9297f59f23d5430 +Author: Anthony Green +Date: Sat Aug 8 17:09:22 2026 -0400 + + README: note i386 BSD small-struct register return + + Follow-up to #1010, which returns small structs in registers on i386 + FreeBSD/OpenBSD but did not update the History section. + + Co-Authored-By: Claude Opus 4.8 (1M context) + +commit f744bc303fa4c69f1202ce283b866ebc768e0432 +Merge: abc18be0 5b8fa3fe +Author: Anthony Green +Date: Sat Aug 8 17:09:02 2026 -0400 + + Merge pull request #1010 from DTW-Thalion/x86-bsd-small-struct-return + + x86: return small structs in registers on the BSD i386 targets + +commit abc18be0d9ba9cc37c955b317e63cd52fd0d90ee +Merge: ed742112 5f24e6a0 +Author: Anthony Green +Date: Sat Aug 8 16:57:36 2026 -0400 + + Merge pull request #1006 from rvandermeulen/call-plan-size + + call_plan: add ffi_call_plan_size to report a plan's allocation + +commit ed7421122880e4daff87f1c8623d508a2e2c5c9a +Merge: e43f2548 e6db2d38 +Author: Anthony Green +Date: Sat Aug 8 16:41:24 2026 -0400 + + Merge pull request #1009 from libffi/fix-jumptable-desync-family + + Fix FFI_TYPE_LAST/vector jump-table desyncs on ia64, ppc64 (BE ELFv2), and aarch64 + +commit e6db2d38decee8bf6321472dff5147ad311e639a +Author: Anthony Green +Date: Fri Aug 7 17:00:49 2026 -0400 + + aarch64: reject sub-4-byte vector lanes in HVA classification + + is_vfp_type() maps a homogeneous vector aggregate's lane width onto the + S/D/Q register classes via FFI_TYPE_FLOAT + intlog2(reg_size) - 2, and + encodes the result as an AARCH64_RET_* code. A lane narrower than 4 bytes + (e.g. a struct of two 2-byte vectors, which libffi's own initialize_vector + accepts) yields intlog2(reg_size) < 2, producing a code below + AARCH64_RET_S4. extend_hfa_type() then computes a negative jump-table + offset (h - AARCH64_RET_S4) and branches before its table -- a wild + computed branch during ffi_call argument marshalling. + + Such a type has no short-vector register class under AAPCS64, so reject it + in is_vfp_type() (returning 0 routes it through the generic aggregate + path). Fixing it at the source covers both the argument path + (extend_hfa_type) and the return path. Verified on aarch64 (Fedora under + qemu-aarch64): a call passing such an HVA segfaults before the fix and + returns correctly after it. + + Co-Authored-By: Claude Opus 4.8 (1M context) + +commit 159268174ece06f6854c6d1bca1a9b95961f6ae9 +Author: Anthony Green +Date: Fri Aug 7 08:18:51 2026 -0400 + + powerpc64: fix big-endian ELFv2 closure returns of 5/6/7-byte structs + + On big-endian ppc64 ELFv2, ffi_closure_helper_LINUX64 returns the load + codes PPC64_LD_STRUCT_5/6/7 (17/18/19) for closures returning a 5-, 6-, + or 7-byte struct, but linux64_closure.S only defined return jump-table + entries through PPC64_LD_STRUCT_3 (16). The E() macro places each 16-byte + slot with .align 4 (no .org), so codes 17/18/19 fell through into the + .Lmoredouble continuation: the closure loaded FP registers and returned + without writing r3, so the ELFv2 caller read back the computed jump + target -- a libffi code address -- as the struct value (wrong result plus + a code-pointer disclosure). Little-endian ELFv2 is unaffected (those + codes alias PPC_LD_R3/I64); big-endian ELFv1 returns structs by reference + and never emits the codes. + + Add the three missing handlers, loading the struct right-justified into + r3 per the ELFv2 convention. Verified on big-endian ppc64 ELFv2 (Adélie + Linux under qemu-ppc64): testsuite/libffi.closures/cls_{5,6,7}_1_byte.c + abort before the fix and pass after it. + + Co-Authored-By: Claude Opus 4.8 (1M context) + +commit 3fdd99b5d2fb4c8f940d82fc4b1e530e743c685b +Author: Anthony Green +Date: Fri Aug 7 06:11:00 2026 -0400 + + ia64: fix return jump-table desync after FFI_TYPE_LAST bump + + The .Lst_table/.Lld_table return-value dispatch tables in unix.S are + indexed by the FFI_IA64_TYPE_SMALL_STRUCT/HFA_* codes, which are + FFI_TYPE_LAST-relative, but the tables hardcoded 20 entries assuming + FFI_TYPE_LAST == FFI_TYPE_COMPLEX (15). The conditional __int128 + support added in 3.6.0 advanced FFI_TYPE_LAST to SINT128 (17), and + FFI_TYPE_VECTOR advanced it to 18, shifting SMALL_STRUCT to 19 -- so a + small-struct return dispatched to the HFA-ldouble handler's 16-byte + stfe store, an out-of-bounds write past rvalue, and HFA returns indexed + off the end of the table entirely. + + Add the missing UINT128/SINT128/VECTOR slots to both tables (pointing at + the existing not-implemented void handler, matching FFI_TYPE_COMPLEX) + and a FFI_TYPE_LAST tripwire, mirroring the pa and win64 guards. + + Co-Authored-By: Claude Opus 4.8 (1M context) + +commit 5b8fa3fed84ce17768eeb379f5b81663172482e9 +Author: Todd White +Date: Fri Aug 7 19:40:05 2026 -0400 + + x86: return small structs in registers on the BSD i386 targets + + i386 FreeBSD and OpenBSD return a struct of 1, 2, 4 or 8 bytes in eax and + edx, as Darwin and win32 do. ffi_prep_cif_machdep applied the size test + only under X86_WIN32 and X86_DARWIN, so on these targets it recorded + X86_RET_STRUCTPOP and allocated a return pointer the callee never writes, + and a struct return through ffi_call or through a closure read a value + that was never stored. + + configure.host already maps i?86-*-freebsd* and i?86-*-openbsd* to + TARGET=X86_FREEBSD, and include/ffi.h.in defines that name, so extend the + condition to it. Sizes 3, 6 and 12 continue to be returned in memory. + +commit 5f24e6a05574b1aa74cca77b1ecd6413a8105f62 +Author: Ryan VanderMeulen +Date: Wed Aug 5 11:09:51 2026 -0400 + + call_plan: add ffi_call_plan_size to report a plan's allocation + + ffi_call_plan is opaque, so an embedder that tracks the memory a long-lived + plan holds has no way to ask how big it is. The only options are to hardcode + a guess or to hardcode knowledge of the private struct layout, and both go + stale silently on the next release. + + The x86-64 backend records the byte count in ffi_plan at the point it is + passed to malloc, so the reported value cannot drift from the allocation it + describes; ffi_call_plan_size adds that to the handle and treats a signature + with no fast path as owning nothing beyond it. The generic backend's plan is + a bare handle, so it reports sizeof (struct ffi_call_plan). The counter lives + in ffi_plan rather than in the handle so that only plans that actually own a + move-list pay for it, and plans without one pay nothing. + + Computing the size in the query from cif->nargs instead would duplicate + build_plan's allocation formula in a second place, and would report the wrong + number if the cif were re-prepared with a different argument count after the + plan was built. + + The new symbol gets its own version node rather than joining + LIBFFI_CALL_PLAN_8.4, which shipped in 3.7.0: adding to a released node would + let a binary that needs ffi_call_plan_size look satisfiable against a 3.7.x + library that exports the node without the symbol, turning a clean link error + into a runtime failure. libtool-version is left alone, since rule 2 in that + file defers version updates to immediately before a release. + +commit e43f254881f9010a26c48f595928461c0432c7b4 +Merge: 2fd434cd 04d721cc +Author: Anthony Green +Date: Thu Aug 6 00:40:32 2026 -0400 + + Merge pull request #1008 from libffi/fix-win64-vector-small-struct-flags + + x86: fix Win64 small-struct returns broken by FFI_TYPE_VECTOR + +commit 04d721cc448316dbba5af506be46315efabd80e3 +Author: Anthony Green +Date: Wed Aug 5 22:59:29 2026 -0400 + + x86: fix Win64 small-struct returns broken by FFI_TYPE_VECTOR + + Adding FFI_TYPE_VECTOR (#1000) moved FFI_TYPE_LAST from FFI_TYPE_SINT128 + (17) to FFI_TYPE_VECTOR (18). The Win64 return pseudo-types + + FFI_TYPE_SMALL_STRUCT_1B/2B/4B = FFI_TYPE_LAST + 1..3 + + are FFI_TYPE_LAST-relative, so they shifted from 18/19/20 to 19/20/21. + The win64.S / win64_intel.S return-value dispatch is a computed jump + table indexed by cif->flags (base + flags*8) whose handlers are emitted + contiguously right after FFI_TYPE_SINT128, with no slot for value 18. + Under the sequential E() variant used by the MSVC/ml64 build, the + size-1/2/4 small-struct handlers therefore sat one 8-byte slot below the + flag values ffiw64.c now emits, so small structs returned by value were + written with the wrong width (or fell off the table into abort). This + showed up as 14 execution failures in the "Windows 64-bit Visual C++" CI + job (s55, struct3, struct_by_value_small, struct_return_2H, the small + cls_* / single_entry_structs closures, and bhaible DGTEST 47/53/55). + + Add an FFI_TYPE_VECTOR abort stub between SINT128 and SMALL_STRUCT_1B in + both tables so the jump table stays contiguous and the small-struct + entries realign with their (shifted) code values. Win64 does not marshal + vectors -- ffi_prep_cif_core rejects them since FFI_TARGET_HAS_VECTOR_TYPE + is undefined there -- so the slot is never reached at runtime. + + Also add a pa-style compile-time tripwire to src/x86/ffitarget.h so the + next generic type added bumps FFI_TYPE_LAST and #errors until the win64 + tables are updated in step. 32-bit x86 is unaffected: sysv.S indexes its + store table by the independent X86_RET_* enum, not FFI_TYPE_LAST. + + Co-Authored-By: Claude Opus 4.8 (1M context) + +commit 2fd434cd9ada4d3d97b355e62c3ce3a969682230 +Merge: a00279c2 2aa33761 +Author: Anthony Green +Date: Sun Aug 2 10:18:45 2026 -0400 + + Merge pull request #1005 from libffi/tramp-cache-unsupported-verdict + + tramp: cache the static trampoline "unsupported" verdict + +commit 2aa33761c0536339f9f322902b9bb3a981114724 +Merge: 76883c62 a00279c2 +Author: Anthony Green +Date: Sun Aug 2 10:18:33 2026 -0400 + + Merge branch 'master' into tramp-cache-unsupported-verdict + +commit 76883c628a5273f71fb025e75bf1076adae3bb4b +Author: Anthony Green +Date: Sun Aug 2 10:05:22 2026 -0400 + + tramp: cache the static trampoline "unsupported" verdict + + ffi_tramp_init() bailed out with a plain `return 0` when the system page + size exceeds the trampoline code table mapping, without recording the + outcome in tramp_globals.status. Because that early return was the only + failure exit that left status as UNINITIALIZED, every subsequent + ffi_tramp_alloc()/ffi_tramp_is_supported() call re-ran the full + initialization (ffi_tramp_arch(), sysconf(), etc.) instead of + short-circuiting on the cached verdict like the other two failure paths. + + The comparison is between two process-lifetime invariants -- map_size is + a compile-time constant from ffi_tramp_arch(), and page_size is fixed for + the life of the process (and only checked when sysconf() returned a valid + value) -- so it can never flip. Caching FAILED is therefore safe and + matches the intent of the status field. + + Affects hosts with pages larger than the 16K table, in practice 64K-page + aarch64 kernels, where static trampolines are correctly declined but the + decline was recomputed on every closure allocation. No functional change + on 4K/16K-page hosts. + + Co-Authored-By: Claude Opus 4.8 (1M context) + +commit a00279c2dc8e191ae5136b46bf6ae0e7a8da5b7a +Author: Anthony Green +Date: Sat Aug 1 07:17:23 2026 -0400 + + Note unreleased development changes in README history + + Add a "Development source only" History block for changes on master + since 3.7.1: FFI_TYPE_VECTOR SIMD support (#1000), powerpc64 _Complex + long double (#1003), and the powerpc Darwin closure fix (#1002). + + Co-Authored-By: Claude Opus 4.8 (1M context) + +commit ce77ca107a5cb0d10d5525c9422f0207e6c79ebf +Merge: d257b084 19dbdb53 +Author: Anthony Green +Date: Sat Aug 1 07:09:17 2026 -0400 + + Merge pull request #1000 from edusperoni/feat/vector-types + + Add FFI_TYPE_VECTOR: vector (SIMD) type support with libffi-computed layout + +commit d257b08495e95248f66b7bd50dd106ea19124df9 +Merge: 333d87cf b2170647 +Author: Anthony Green +Date: Tue Jul 28 00:47:29 2026 -0400 + + Merge pull request #1004 from libffi/fix-1002-ppc-darwin-closure + + powerpc: fix Darwin closure returns broken by #951 + +commit b2170647583461f42dc2d9f201211fcafda2429f +Author: Anthony Green +Date: Mon Jul 27 20:08:15 2026 -0400 + + powerpc: fix Darwin closure returns broken by #951 + + PR #951 (840add3b) changed the shared PowerPC closure helper, + ffi_closure_helper_common, to return a small PPC_LD_* jump-table index + instead of the ffi_type*, and rewrote aix_closure.S to consume it -- but + left darwin_closure.S expecting the old ffi_type* and dereferencing it. + With the helper now returning a small integer, ffi_closure_ASM + dereferenced e.g. 0 (PPC_LD_NONE, a void return) as a pointer, faulting + on a load from address 0. This crashed essentially every closure call + -- including every gobject-introspection signal handler -- on 32- and + 64-bit PowerPC Darwin (SIGBUS at ffi_closure_ASM, dar=0; issue #1002). + + Convert darwin_closure.S to the PPC_LD_* convention, mirroring + aix_closure.S: drop the ffi_type* dereference, use the returned index + directly, and reorder the return-value jump table into PPC_LD_* order + (NONE, R3, R3R4, F32, F64, F128, U8, S8, U16, S16, and on ppc64 U32, S32). + + Darwin, unlike AIX, returns small structs by value in registers, which + the existing assembly handles (Lsmallstruct/Lfour/Lstructend). The + helper's return code is a single small integer with no room for + cif->rtype, which that assembly needs, so for a by-value struct return + the helper now stashes cif->rtype in the first parameter-save slot (dead + by return time) and returns a new PPC_LD_STRUCT code; the PPC_LD_STRUCT + fragment recovers it and drives the unchanged struct machinery. By- + reference struct returns still return PPC_LD_NONE. + + Based on the approach in a patch by Sergey Fedorov (@barracuda156); the + jump table here is reordered to the PPC_LD_* layout so that float, + double, long double, sub-word and 64-bit returns also dispatch correctly. + + I have no PowerPC Darwin hardware; the jump-table fragment offsets were + checked by assembling for powerpc and powerpc64, but runtime + confirmation on 10.5/10.6 is still needed. + + Fixes #1002. + + Co-Authored-By: Claude Opus 4.8 (1M context) + +commit 333d87cf201ee279c9870fb5ad3e48e3a08aa6e5 +Merge: 46cb2e38 d7cd3a61 +Author: Anthony Green +Date: Mon Jul 27 08:52:34 2026 -0400 + + Merge pull request #1003 from libffi/fix-ppc64le-complex-longdouble + + powerpc64: implement _Complex long double for both IBM-128 and IEEE-128 + +commit d7cd3a6194885c85255c77782a05a153a42b29a5 +Author: Anthony Green +Date: Mon Jul 27 07:06:39 2026 -0400 + + powerpc64: implement _Complex long double for both IBM-128 and IEEE-128 + + Complex support for POWERPC64 ELFv2 (f0ca157, #970) defined + FFI_TARGET_HAS_COMPLEX_TYPE, which flips complex.exp from marking the + libffi.complex suite UNSUPPORTED to running it. _Complex long double + was deliberately deferred with FFI_BAD_TYPEDEF, so ffi_prep_cif failed + and every libffi.complex/*longdouble* test aborted. This was not caught + upstream because an XFAIL entry in the rlgl CI policy masked the FAILs. + + Implement both long double formats: + + - IBM-128 (double-double): each _Complex long double is passed and + returned as four doubles (real hi/lo, imag hi/lo) in f1-f4, with a + GPR shadow doubleword per FPR, and returned as a double homogeneous + aggregate. + + - IEEE binary128: real in v2, imag in v3; each half occupies a vector + register (or a 16-byte-aligned parameter save slot with two GPR + shadow doublewords) and is returned via the vector-homogeneous + small-struct path. + + discover_homogeneous_aggregate now accepts FFI_TYPE_LONGDOUBLE as a + _Complex inner type so struct-of-complex-longdouble is treated as an HFA. + Covers ffi_prep_cif, ffi_prep_args64, and the closure decode/return + paths. + + Fixes #1001. + + Co-Authored-By: Claude Opus 4.8 (1M context) + +commit debc00a0114d8530d6d691862028c607aa17dd6a +Author: Anthony Green +Date: Sun Jul 26 07:28:36 2026 -0400 + + Update doc version + +commit 19dbdb53e869e07fbff05c86d634e8c08c9a7f61 +Author: Eduardo Speroni +Date: Tue Jul 21 20:32:24 2026 -0300 + + testsuite: fix vector suite CI failures on gcc and MSVC + + Two fixes for the libffi.vector suite: + + - vector_double4.c: the non-aarch64 branch built its own void_args + array and never read the already-populated args, tripping gcc's + -Wunused-but-set-variable (an excess-errors FAIL on Linux x86-64 + with gcc; clang does not emit this warning). Use args for the + negative argument-passing check instead. + + - vector.exp: the suite only probed FFI_TARGET_HAS_VECTOR_TYPE, but + the tests are written with the GCC/Clang vector extension. On + Windows ARM64 the aarch64 port enables the feature while MSVC + cannot compile __attribute__ ((vector_size)), so every test failed + to build. Add a compile probe and mark the suite unsupported when + the compiler lacks the syntax. + +commit 71a95a2cd433b151b3fcf83a0a830eb9aa38fa3a +Author: Eduardo Speroni +Date: Tue Jul 21 16:44:26 2026 -0300 + + testsuite: add libffi.vector suite for vector (SIMD) types + + Model a new testsuite/libffi.vector/ directory on testsuite/libffi.complex: + vector.exp reuses the same dg/run-many-tests driver and skips every test as + "unsupported" on ports whose headers do not define + FFI_TARGET_HAS_VECTOR_TYPE (libffi_feature_test), so unsupported targets + still compile the gating cleanly. + + Vector types are built with the portable __attribute__((vector_size)) via a + small make_vector_type() helper (vector.h); each test cross-checks the value + returned through ffi against a direct native call. Coverage: + + - vector_float32x4 / vector_float32x2 / vector_double2 / vector_int32x4: + pass and return 8- and 16-byte float, double and integer vectors + (float32x4 is the vec4 shape of libffi/libffi#773); + - vector_args_spill: ten vectors interleaved with int/double scalars, + exhausting the vector argument registers and spilling to the stack; + - vector_vec3: Clang-only ext_vector_type(3), verifying the 12->16 byte + power-of-two padding matches a natively compiled callee (a no-op on + other compilers); + - vector_double4: on AArch64 a 32-byte vector round-trips (by reference / + in memory); elsewhere ffi_prep_cif must return FFI_BAD_TYPEDEF, checked + for both return and argument; + - vector_hva: a struct of two identical vectors (HVA) passes and returns + on both AArch64 (Q-register pair) and x86-64 (SSE struct classification); + - cls_vector: a closure receiving vector arguments and returning a vector; + - vector_validate: heterogeneous lanes, an empty vector, and a non-scalar + lane are each rejected with FFI_BAD_TYPEDEF, and a well-formed vector is + accepted with the computed power-of-two size and min(size,16) alignment. + + The files are added to testsuite/Makefile.am EXTRA_DIST, matching how + libffi.complex is distributed. + + References: libffi/libffi#414, libffi/libffi#773. + +commit 93b274cec912ac2395575a8bd4dfcb527f61599c +Author: Eduardo Speroni +Date: Tue Jul 21 16:32:04 2026 -0300 + + x86-64: marshal vector (SIMD) types per the System V psABI + + Define FFI_TARGET_HAS_VECTOR_TYPE for the SysV x86-64 backend (ffi64.c; + 32-bit x86 and the Windows ffiw64.c backend are excluded) and integrate + FFI_TYPE_VECTOR into the existing psABI classifier without restructuring + it: + + - classify_argument gains a FFI_TYPE_VECTOR case: an 8-byte vector is + one SSE eightbyte (X86_64_SSE_CLASS); a 16-byte vector is one %xmm + register (X86_64_SSE_CLASS + X86_64_SSEUP_CLASS). The existing + INTEGERSI/SSESF/SSEDF/UINT128 handling is untouched, and the SSE+SSEUP + argument marshalling already merges both eightbytes into one %xmm. + - ffi_prep_cif_machdep classifies vector returns symmetrically: 8 bytes + in %xmm0 (UNIX64_RET_XMM64), 16 bytes in %xmm0 (UNIX64_RET_XMM128). + - Vectors wider than 16 bytes return FFI_BAD_TYPEDEF from + ffi_prep_cif_machdep, for both returns and arguments. Correct + %ymm/%zmm passing needs unix64.S register-save changes and is left as + a v1 limitation rather than silently passing them in memory. + + Closures need no separate change: the closure paths reuse + classify_argument for arguments and cif->flags for the return. + + References: libffi/libffi#414. + +commit 5eaa8a389de61fc3b056f62c48ceade1931b5413 +Author: Eduardo Speroni +Date: Tue Jul 21 16:30:12 2026 -0300 + + aarch64: marshal vector (SIMD) types per AAPCS64 + + Define FFI_TARGET_HAS_VECTOR_TYPE for AArch64 and teach is_vfp_type to + classify FFI_TYPE_VECTOR, so ffi_call and closures pass and return + vectors the way AAPCS64 (and current GCC/Clang) do: + + - 8- and 16-byte vectors travel in a single V/Q register (a Short + Vector), for float, double and integer lane types alike; + - homogeneous vector aggregates -- a struct of up to four identical + 8- or 16-byte vectors -- travel in that many consecutive V/Q + registers (an HVA), e.g. struct{float32x4 a,b} in {q0,q1}; + - a bare vector wider than 16 bytes (e.g. a 32-byte double4) has no + short-vector register class, so is_vfp_type returns 0 and the + existing composite path passes it by invisible reference and returns + it in memory -- exactly what a natively compiled callee expects. + + is_simd() reports the width of one Neon register slot (a bare vector's + whole size, or one lane vector of an HVA); is_vfp_type() encodes + num_registers slots of that width onto the existing AARCH64_RET_{D,Q}* + codes via intlog2. is_hfa0/is_hfa1 recurse through FFI_TYPE_VECTOR so + HVA homogeneity is checked, and the three fundamental-type dispatch + switches (machdep return, ffi_call_int, ffi_closure_SYSV_inner) route + FFI_TYPE_VECTOR through is_vfp_type alongside FFI_TYPE_STRUCT. + + Ported from the battle-tested NativeScript aarch64 vector marshaller, + adapted to the FFI_TYPE_VECTOR API and extended so that integer-lane + vectors (e.g. int32x4) are classified into V registers too -- the + original only handled floating-point lanes. + + References: libffi/libffi#414, libffi/libffi#773 (aarch64 vec4 return). + +commit b6b8be54acc90f7db1dcf3d1c91238a5a9bca185 +Author: Eduardo Speroni +Date: Tue Jul 21 16:26:33 2026 -0300 + + core: add FFI_TYPE_VECTOR fundamental type with computed layout + + Introduce a portable API for marshalling vector (SIMD) types -- the + values produced by GCC's __attribute__((vector_size)) and Clang's + ext_vector_type. This answers the stalled PR #414 and the maintainer's + 2018 design questions + (https://sourceware.org/legacy-ml/libffi-discuss/2018/msg00020.html): + rather than requiring callers to hand-compute a vector's size and + alignment (and gating the feature behind configure), libffi now derives + the layout itself and the type code is defined unconditionally. + + A vector is described exactly like a struct: type == FFI_TYPE_VECTOR and + a NULL-terminated elements[] array, except every element must point to + the SAME fundamental scalar (float, double, or a fixed-width integer + UINT8..SINT64) and the count is the number of lanes. The caller leaves + size and alignment at zero; ffi_prep_cif computes: + + size = lane_size * count, rounded up to the next power of two + (matches Clang ext_vector_type storage: 3 x float -> 16, + 3 x double -> 32; GCC vector_size already requires pow2 + totals so it is identical there); + alignment = min(size, 16). + + Validation (identical scalar lanes, count >= 1, scalar-only) yields + FFI_BAD_TYPEDEF otherwise. + + - include/ffi.h.in: FFI_TYPE_VECTOR = 18 (after SINT128 = 17), + FFI_TYPE_LAST bumped. Defined unconditionally, no configure gating. + - src/prep_cif.c: initialize_vector() computes the layout in + initialize_aggregate; ffi_type_contains_vector() rejects vectors + (including nested in structs, argument or return) with + FFI_BAD_TYPEDEF on any port that does not define + FFI_TARGET_HAS_VECTOR_TYPE -- no aborts. Vector returns reserve the + hidden return-pointer slot like structs. + - src/raw_api.c, src/java_raw_api.c: plumb FFI_TYPE_VECTOR alongside + FFI_TYPE_STRUCT, mirroring how FFI_TYPE_COMPLEX is handled. + - src/debug.c: ffi_type_test requires elements != NULL for vectors. + - src/pa/ffitarget.h: bump the FFI_PA_TYPE_LAST tripwire; PA gates + vectors out in prep_cif so its jump tables are never reached. + - doc/libffi.texi: new "Vector Types" node documenting the API, the + computed-layout rule, the psABI framing, and the per-port support + table. + + No port defines FFI_TARGET_HAS_VECTOR_TYPE yet, so this commit rejects + every vector signature; the per-architecture ports follow. + + References: libffi/libffi#414, libffi/libffi#773. + +commit 46cb2e3871059f7f5113329ddcca818de3a8cfae +Merge: ca86812c 8cd11a77 +Author: Anthony Green +Date: Fri Jul 10 16:51:18 2026 -0400 + + Merge pull request #998 from bgilbert/tests + + testsuite: Remember to distribute tests added for 3.7.1 + +commit 8cd11a772d8a0b687f43390697baa002ae6504d5 +Author: Benjamin Gilbert +Date: Fri Jul 10 11:56:55 2026 -0700 + + testsuite: Remember to distribute tests added for 3.7.1 + +commit ca86812cd430cff3018e491ba75a4f3c9ea969d2 +Author: Anthony Green +Date: Fri Jul 10 10:56:47 2026 -0400 + + ci: Don't publish rlgl reports on tag pushes + + A tag and its commit fire two CI runs at the same SHA. Both run the + publish-reports job, which deploys a fixed-name github-pages artifact + via actions/deploy-pages; the two deployments collide and one fails + with BlobNotFound (seen on the v3.7.1 tag run). The same-SHA branch + push already publishes the reports, so gate the job off tag pushes. + + Co-Authored-By: Claude Fable 5 + commit 5c1c43091ed611fdea774374355eb938c73a9157 Author: Anthony Green Date: Fri Jul 10 09:50:53 2026 -0400 diff --git a/deps/libffi/README.md b/deps/libffi/README.md index 19f78f632b0c..797d0fd9aa8e 100644 --- a/deps/libffi/README.md +++ b/deps/libffi/README.md @@ -201,6 +201,28 @@ History See the git log for details at http://github.com/libffi/libffi. + 3.8.0 August-8-2026 + Add FFI_TYPE_VECTOR (SIMD) type support with libffi-computed + layout, for aarch64 and x86-64 (#1000, closes #773). + Add ffi_call_plan_size to report the total memory a reusable call + plan owns, for embedders that account for the memory held by + long-lived plans. + Add powerpc64 ELFv2 _Complex long double support for both + IBM-128 (double-double) and IEEE-128 formats (#1003, closes #1001). + Fix powerpc64 big-endian ELFv2 closures returning 5-, 6-, or + 7-byte structs: missing return jump-table entries produced a + wrong result and leaked a libffi code pointer. + Fix ia64 return-value jump-table desync after the FFI_TYPE_LAST + bump, which corrupted small-struct and HFA returns. + Fix powerpc Darwin closure returns broken by #951 (#1002). + Return small (1, 2, 4 or 8 byte) structs in registers on the i386 + FreeBSD and OpenBSD targets, matching the platform ABI and + fixing a segfault on struct returns through ffi_call and closures. + Cache the static trampoline "unsupported" result on hosts whose + page size exceeds the trampoline table mapping, avoiding + redundant re-initialization on every closure allocation + (e.g. 64K-page aarch64). + 3.7.1 July-10-2026 Fix aarch64 ffi_call memory corruption when passing many large structs by value. diff --git a/deps/libffi/configure b/deps/libffi/configure index e050ddc8675d..7b8a4cb375d1 100755 --- a/deps/libffi/configure +++ b/deps/libffi/configure @@ -1,6 +1,6 @@ #! /bin/sh # Guess values for system-dependent variables and create Makefiles. -# Generated by GNU Autoconf 2.71 for libffi 3.7.1. +# Generated by GNU Autoconf 2.71 for libffi 3.8.0. # # Report bugs to . # @@ -621,8 +621,8 @@ MAKEFLAGS= # Identity of this package. PACKAGE_NAME='libffi' PACKAGE_TARNAME='libffi' -PACKAGE_VERSION='3.7.1' -PACKAGE_STRING='libffi 3.7.1' +PACKAGE_VERSION='3.8.0' +PACKAGE_STRING='libffi 3.8.0' PACKAGE_BUGREPORT='http://github.com/libffi/libffi/issues' PACKAGE_URL='' @@ -1417,7 +1417,7 @@ if test "$ac_init_help" = "long"; then # Omit some internal or obsolete options to make the list less imposing. # This message is too long to be a string in the A/UX 3.1 sh. cat <<_ACEOF -\`configure' configures libffi 3.7.1 to adapt to many kinds of systems. +\`configure' configures libffi 3.8.0 to adapt to many kinds of systems. Usage: $0 [OPTION]... [VAR=VALUE]... @@ -1489,7 +1489,7 @@ fi if test -n "$ac_init_help"; then case $ac_init_help in - short | recursive ) echo "Configuration of libffi 3.7.1:";; + short | recursive ) echo "Configuration of libffi 3.8.0:";; esac cat <<\_ACEOF @@ -1628,7 +1628,7 @@ fi test -n "$ac_init_help" && exit $ac_status if $ac_init_version; then cat <<\_ACEOF -libffi configure 3.7.1 +libffi configure 3.8.0 generated by GNU Autoconf 2.71 Copyright (C) 2021 Free Software Foundation, Inc. @@ -2259,7 +2259,7 @@ cat >config.log <<_ACEOF This file contains any messages produced by compilers while running configure, to aid debugging if configure makes a mistake. -It was created by libffi $as_me 3.7.1, which was +It was created by libffi $as_me 3.8.0, which was generated by GNU Autoconf 2.71. Invocation command line was $ $0$ac_configure_args_raw @@ -3233,10 +3233,10 @@ ac_compiler_gnu=$ac_cv_c_compiler_gnu ac_config_headers="$ac_config_headers fficonfig.h" -FFI_VERSION_STRING="3.7.1" -ffi_version_major=`echo "3.7.1" | cut -d. -f1` -ffi_version_minor=`echo "3.7.1" | cut -d. -f2 | sed 's/[^0-9].*//'` -ffi_version_micro=`echo "3.7.1" | cut -d. -f3 | sed 's/[^0-9].*//'` +FFI_VERSION_STRING="3.8.0" +ffi_version_major=`echo "3.8.0" | cut -d. -f1` +ffi_version_minor=`echo "3.8.0" | cut -d. -f2 | sed 's/[^0-9].*//'` +ffi_version_micro=`echo "3.8.0" | cut -d. -f3 | sed 's/[^0-9].*//'` FFI_VERSION_NUMBER=`expr ${ffi_version_major:-0} \* 10000 + ${ffi_version_minor:-0} \* 100 + ${ffi_version_micro:-0}` @@ -3986,7 +3986,7 @@ fi # Define the identity of the package. PACKAGE='libffi' - VERSION='3.7.1' + VERSION='3.8.0' printf "%s\n" "#define PACKAGE \"$PACKAGE\"" >>confdefs.h @@ -20661,7 +20661,7 @@ cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 # report actual input values of CONFIG_FILES etc. instead of their # values after options handling. ac_log=" -This file was extended by libffi $as_me 3.7.1, which was +This file was extended by libffi $as_me 3.8.0, which was generated by GNU Autoconf 2.71. Invocation command line was CONFIG_FILES = $CONFIG_FILES @@ -20729,7 +20729,7 @@ ac_cs_config_escaped=`printf "%s\n" "$ac_cs_config" | sed "s/^ //; s/'/'\\\\\\\\ cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 ac_cs_config='$ac_cs_config_escaped' ac_cs_version="\\ -libffi config.status 3.7.1 +libffi config.status 3.8.0 configured by $0, generated by GNU Autoconf 2.71, with options \\"\$ac_cs_config\\" diff --git a/deps/libffi/configure.ac b/deps/libffi/configure.ac index 3370acc3a396..826b453b8353 100644 --- a/deps/libffi/configure.ac +++ b/deps/libffi/configure.ac @@ -2,7 +2,7 @@ dnl Process this with autoconf to create configure AC_PREREQ([2.68]) -AC_INIT([libffi],[3.7.1],[http://github.com/libffi/libffi/issues]) +AC_INIT([libffi],[3.8.0],[http://github.com/libffi/libffi/issues]) AC_CONFIG_HEADERS([fficonfig.h]) dnl Derive the version macros from AC_INIT so they cannot drift when the diff --git a/deps/libffi/doc/libffi.info b/deps/libffi/doc/libffi.info index b43246f8ed4e..4c1abc85f777 100644 --- a/deps/libffi/doc/libffi.info +++ b/deps/libffi/doc/libffi.info @@ -301,6 +301,7 @@ File: libffi.info, Node: Types, Next: Multiple ABIs, Prev: Simple Example, U * Type Example:: Structure type example. * Complex:: Complex types. * Complex Type Example:: Complex type example. +* Vector Types:: Vector (SIMD) types.  File: libffi.info, Node: Primitive Types, Next: Structures, Up: Types @@ -660,7 +661,7 @@ functions ‘ffi_prep_cif’ and ‘ffi_prep_args’ abort the program if they encounter a complex type.  -File: libffi.info, Node: Complex Type Example, Prev: Complex, Up: Types +File: libffi.info, Node: Complex Type Example, Next: Vector Types, Prev: Complex, Up: Types 2.3.7 Complex Type Example -------------------------- @@ -746,6 +747,92 @@ compilers that support them: The new type descriptors can then be used like one of the built-in type descriptors in the previous example. + +File: libffi.info, Node: Vector Types, Prev: Complex Type Example, Up: Types + +2.3.8 Vector Types +------------------ + +‘libffi’ can marshal vector (SIMD) types -- the values produced by GCC's +‘__attribute__((vector_size (N)))’ and Clang's ‘ext_vector_type’ -- on +the platforms listed in the support table below. A vector is described +just like a structure, except that every element pointer refers to the +_same_ fundamental scalar type and the number of elements is the number +of vector lanes. + + -- Data type: ffi_type + ‘size_t size’ + This must be set to ‘0’. ‘libffi’ computes the storage size + (see below) from the element type and lane count. + + ‘unsigned short alignment’ + This must be set to ‘0’. ‘libffi’ computes the alignment. + + ‘unsigned short type’ + For a vector type, this must be set to ‘FFI_TYPE_VECTOR’. + + ‘ffi_type **elements’ + This is a ‘NULL’-terminated array of pointers to ‘ffi_type’ + objects. Every entry must point to the same scalar element + type, and the number of entries is the vector's lane count N + (N >= 1). The element type must be one of ‘ffi_type_float’, + ‘ffi_type_double’, or a fixed-width integer (‘ffi_type_uint8’ + through ‘ffi_type_sint64’); ‘long double’ and aggregate + element types are not permitted. + +Computed layout +............... + +Because the caller leaves ‘size’ and ‘alignment’ at ‘0’, ‘libffi’ +derives them so that applications need not encode compiler- or +platform-specific rules: + + • ‘size’ is lane\_size \times N rounded _up_ to the next power of + two. This matches Clang's ‘ext_vector_type’ storage -- for example + a three-lane ‘float’ vector occupies 16 bytes and a three-lane + ‘double’ vector occupies 32 bytes. GCC's ‘vector_size’ already + requires power-of-two byte totals, so the rule is identical there. + + • ‘alignment’ is ‘min(size, 16)’. + + If the element list is heterogeneous, empty, or uses a disallowed +element type, ‘ffi_prep_cif’ returns ‘FFI_BAD_TYPEDEF’. + +psABI framing +............. + +At the call boundary the platform's processor-specific ABI (AAPCS64 on +AArch64, the System V x86-64 psABI on x86-64) decides how a vector is +passed and returned, independently of which compiler produced it. The +historical divergence between GCC's ‘vector_size’ and Clang's +‘ext_vector_type’ concerns only in-memory _layout_ (notably the padding +of odd-lane vectors such as ‘float3’); the power-of-two size rule above +pins that layout down, so a value marshalled by ‘libffi’ matches what a +natively compiled caller or callee expects. + +Per-port support +................ + +Port Vector support +-------------------------------------------------------------------------- +AArch64 (AAPCS64) 8- and 16-byte vectors in a single V/Q register; + homogeneous vector aggregates (structs of up to + four identical 8- or 16-byte vectors) in + consecutive V/Q registers. A bare vector larger + than 16 bytes (for example a 32-byte ‘double4’) + has no short-vector register class and is passed + and returned in memory, exactly as AAPCS64 and + current compilers do. +x86-64 (System V 8- and 16-byte vectors in an SSE register (‘%xmm0’ +psABI) for returns). A bare vector larger than 16 bytes + needs ‘%ymm’/‘%zmm’ register handling that this + port does not yet implement, so ‘ffi_prep_cif’ + returns ‘FFI_BAD_TYPEDEF’ for it. +Other ports Not supported: ‘ffi_prep_cif’ returns + ‘FFI_BAD_TYPEDEF’ for any signature that mentions + a vector type, including one nested inside a + struct. +  File: libffi.info, Node: Multiple ABIs, Next: Reusable Call Plans, Prev: Types, Up: Using libffi @@ -795,6 +882,14 @@ prepared ‘ffi_cif’. is harmless. The ‘ffi_cif’ the plan was built from is not affected. + -- Function: size_t ffi_call_plan_size (ffi_call_plan *PLAN) + Returns the total number of bytes ‘libffi’ allocated for PLAN, + including any internal argument-placement data it owns. Returns + zero when PLAN is ‘NULL’. The result does not include the + ‘ffi_cif’, which the caller owns. This is intended for embedders + that account for the memory held by long-lived plans and would + otherwise have to guess at the size of an opaque type. +  File: libffi.info, Node: The Closure API, Next: Closure Example, Prev: Reusable Call Plans, Up: Using libffi @@ -1056,6 +1151,7 @@ Index * ffi_call_plan_alloc: Reusable Call Plans. (line 12) * ffi_call_plan_free: Reusable Call Plans. (line 32) * ffi_call_plan_invoke: Reusable Call Plans. (line 22) +* ffi_call_plan_size: Reusable Call Plans. (line 37) * ffi_closure_alloc: The Closure API. (line 19) * ffi_closure_free: The Closure API. (line 26) * FFI_CLOSURES: The Closure API. (line 13) @@ -1075,6 +1171,8 @@ Index * ffi_type <1>: Structures. (line 10) * ffi_type <2>: Complex. (line 15) * ffi_type <3>: Complex. (line 15) +* ffi_type <4>: Vector Types. (line 13) +* ffi_type <5>: Vector Types. (line 13) * ffi_type_complex_double: Primitive Types. (line 82) * ffi_type_complex_float: Primitive Types. (line 79) * ffi_type_complex_longdouble: Primitive Types. (line 85) @@ -1101,6 +1199,7 @@ Index * ffi_type_void: Primitive Types. (line 10) * Foreign Function Interface: Introduction. (line 31) * size_t: The Basics. (line 125) +* size_t <1>: Reusable Call Plans. (line 37) * unsigned int: The Basics. (line 122) * unsigned long: The Basics. (line 117) * void: The Basics. (line 72) @@ -1118,21 +1217,22 @@ Node: Using libffi4569 Node: The Basics5172 Node: Simple Example11346 Node: Types12403 -Node: Primitive Types12914 -Node: Structures15231 -Node: Size and Alignment16342 -Node: Arrays Unions Enums18613 -Node: Type Example21590 -Node: Complex22896 -Node: Complex Type Example24410 -Node: Multiple ABIs27462 -Node: Reusable Call Plans27849 -Node: The Closure API29566 -Node: Closure Example33908 -Node: Thread Safety35552 -Node: Memory Usage36385 -Node: Missing Features37660 -Node: Index38037 +Node: Primitive Types12967 +Node: Structures15284 +Node: Size and Alignment16395 +Node: Arrays Unions Enums18666 +Node: Type Example21643 +Node: Complex22949 +Node: Complex Type Example24463 +Node: Vector Types27536 +Node: Multiple ABIs31575 +Node: Reusable Call Plans31962 +Node: The Closure API34155 +Node: Closure Example38497 +Node: Thread Safety40141 +Node: Memory Usage40974 +Node: Missing Features42249 +Node: Index42626  End Tag Table diff --git a/deps/libffi/doc/libffi.pdf b/deps/libffi/doc/libffi.pdf index 250d34faf711..75458a8f7d5d 100644 Binary files a/deps/libffi/doc/libffi.pdf and b/deps/libffi/doc/libffi.pdf differ diff --git a/deps/libffi/doc/libffi.texi b/deps/libffi/doc/libffi.texi index 251214f890d3..4d2802c5bd97 100644 --- a/deps/libffi/doc/libffi.texi +++ b/deps/libffi/doc/libffi.texi @@ -320,6 +320,7 @@ int main() * Type Example:: Structure type example. * Complex:: Complex types. * Complex Type Example:: Complex type example. +* Vector Types:: Vector (SIMD) types. @end menu @node Primitive Types @@ -802,6 +803,93 @@ FFI_COMPLEX_TYPEDEF(uchar, unsigned char, ffi_type_uint8); The new type descriptors can then be used like one of the built-in type descriptors in the previous example. +@node Vector Types +@subsection Vector Types + +@code{libffi} can marshal vector (SIMD) types --- the values produced +by GCC's @code{__attribute__((vector_size (N)))} and Clang's +@code{ext_vector_type} --- on the platforms listed in the support table +below. A vector is described just like a structure, except that every +element pointer refers to the @emph{same} fundamental scalar type and the +number of elements is the number of vector lanes. + +@tindex ffi_type +@deftp {Data type} ffi_type +@table @code +@item size_t size +This must be set to @code{0}. @code{libffi} computes the storage size +(see below) from the element type and lane count. + +@item unsigned short alignment +This must be set to @code{0}. @code{libffi} computes the alignment. + +@item unsigned short type +For a vector type, this must be set to @code{FFI_TYPE_VECTOR}. + +@item ffi_type **elements +This is a @samp{NULL}-terminated array of pointers to @code{ffi_type} +objects. Every entry must point to the same scalar element type, and the +number of entries is the vector's lane count @math{N} (@math{N >= 1}). The +element type must be one of @code{ffi_type_float}, @code{ffi_type_double}, +or a fixed-width integer (@code{ffi_type_uint8} through +@code{ffi_type_sint64}); @code{long double} and aggregate element types are +not permitted. +@end table +@end deftp + +@subsubheading Computed layout + +Because the caller leaves @code{size} and @code{alignment} at @code{0}, +@code{libffi} derives them so that applications need not encode +compiler- or platform-specific rules: + +@itemize @bullet +@item +@code{size} is @math{lane\_size \times N} rounded @emph{up} to the next +power of two. This matches Clang's @code{ext_vector_type} storage --- for +example a three-lane @code{float} vector occupies 16 bytes and a three-lane +@code{double} vector occupies 32 bytes. GCC's @code{vector_size} already +requires power-of-two byte totals, so the rule is identical there. + +@item +@code{alignment} is @code{min(size, 16)}. +@end itemize + +If the element list is heterogeneous, empty, or uses a disallowed element +type, @code{ffi_prep_cif} returns @code{FFI_BAD_TYPEDEF}. + +@subsubheading psABI framing + +At the call boundary the platform's processor-specific ABI (AAPCS64 on +AArch64, the System V x86-64 psABI on x86-64) decides how a vector is +passed and returned, independently of which compiler produced it. The +historical divergence between GCC's @code{vector_size} and Clang's +@code{ext_vector_type} concerns only in-memory @emph{layout} (notably the +padding of odd-lane vectors such as @code{float3}); the power-of-two size +rule above pins that layout down, so a value marshalled by @code{libffi} +matches what a natively compiled caller or callee expects. + +@subsubheading Per-port support + +@multitable @columnfractions .28 .72 +@headitem Port @tab Vector support +@item AArch64 (AAPCS64) +@tab 8- and 16-byte vectors in a single V/Q register; homogeneous vector +aggregates (structs of up to four identical 8- or 16-byte vectors) in +consecutive V/Q registers. A bare vector larger than 16 bytes (for +example a 32-byte @code{double4}) has no short-vector register class and is +passed and returned in memory, exactly as AAPCS64 and current compilers do. +@item x86-64 (System V psABI) +@tab 8- and 16-byte vectors in an SSE register (@code{%xmm0} for returns). +A bare vector larger than 16 bytes needs @code{%ymm}/@code{%zmm} register +handling that this port does not yet implement, so @code{ffi_prep_cif} +returns @code{FFI_BAD_TYPEDEF} for it. +@item Other ports +@tab Not supported: @code{ffi_prep_cif} returns @code{FFI_BAD_TYPEDEF} for +any signature that mentions a vector type, including one nested inside a +struct. +@end multitable + @node Multiple ABIs @section Multiple ABIs @@ -853,6 +941,16 @@ Releases a plan returned by @code{ffi_call_plan_alloc}. Passing not affected. @end defun +@findex ffi_call_plan_size +@defun size_t ffi_call_plan_size (ffi_call_plan *@var{plan}) +Returns the total number of bytes @code{libffi} allocated for @var{plan}, +including any internal argument-placement data it owns. Returns zero when +@var{plan} is @code{NULL}. The result does not include the +@code{ffi_cif}, which the caller owns. This is intended for embedders that +account for the memory held by long-lived plans and would otherwise have to +guess at the size of an opaque type. +@end defun + @node The Closure API @section The Closure API diff --git a/deps/libffi/doc/stamp-vti b/deps/libffi/doc/stamp-vti index e755454e9c82..dc281e38e0f6 100644 --- a/deps/libffi/doc/stamp-vti +++ b/deps/libffi/doc/stamp-vti @@ -1,4 +1,4 @@ -@set UPDATED 10 July 2026 -@set UPDATED-MONTH July 2026 -@set EDITION 3.7.1 -@set VERSION 3.7.1 +@set UPDATED 8 August 2026 +@set UPDATED-MONTH August 2026 +@set EDITION 3.8.0 +@set VERSION 3.8.0 diff --git a/deps/libffi/doc/version.texi b/deps/libffi/doc/version.texi index e755454e9c82..dc281e38e0f6 100644 --- a/deps/libffi/doc/version.texi +++ b/deps/libffi/doc/version.texi @@ -1,4 +1,4 @@ -@set UPDATED 10 July 2026 -@set UPDATED-MONTH July 2026 -@set EDITION 3.7.1 -@set VERSION 3.7.1 +@set UPDATED 8 August 2026 +@set UPDATED-MONTH August 2026 +@set EDITION 3.8.0 +@set VERSION 3.8.0 diff --git a/deps/libffi/generate-headers.py b/deps/libffi/generate-headers.py index e2d2942deffb..fb58edf17b66 100644 --- a/deps/libffi/generate-headers.py +++ b/deps/libffi/generate-headers.py @@ -7,8 +7,8 @@ from pathlib import Path -LIBFFI_VERSION = '3.7.1' -LIBFFI_VERSION_NUMBER = '30701' +LIBFFI_VERSION = '3.8.0' +LIBFFI_VERSION_NUMBER = '30800' def normalize_arch(target_arch): aliases = { diff --git a/deps/libffi/include/ffi.h.in b/deps/libffi/include/ffi.h.in index 35f09cf43315..cb0a7dbcaaa3 100644 --- a/deps/libffi/include/ffi.h.in +++ b/deps/libffi/include/ffi.h.in @@ -79,9 +79,10 @@ extern "C" { #define FFI_TYPE_COMPLEX 15 #define FFI_TYPE_UINT128 16 #define FFI_TYPE_SINT128 17 +#define FFI_TYPE_VECTOR 18 /* This should always refer to the last type code (for sanity checks). */ -#define FFI_TYPE_LAST FFI_TYPE_SINT128 +#define FFI_TYPE_LAST FFI_TYPE_VECTOR #include @@ -535,7 +536,11 @@ void ffi_call(ffi_cif *cif, ffi_call_plan_alloc returns NULL only on allocation failure; a signature with no fast path is still valid and ffi_call_plan_invoke falls back to ffi_call for it. A plan is immutable once built, so it may be shared and - invoked concurrently from multiple threads. */ + invoked concurrently from multiple threads. + + ffi_call_plan_size reports the total number of bytes libffi allocated for a + plan, so that callers tracking the footprint of long-lived plans do not have + to guess at the size of an opaque type. */ typedef struct ffi_call_plan ffi_call_plan; FFI_API @@ -550,6 +555,9 @@ void ffi_call_plan_invoke (ffi_call_plan *plan, FFI_API void ffi_call_plan_free (ffi_call_plan *plan); +FFI_API +size_t ffi_call_plan_size (ffi_call_plan *plan); + FFI_API ffi_status ffi_get_struct_offsets (ffi_abi abi, ffi_type *struct_type, size_t *offsets); diff --git a/deps/libffi/libffi.map.in b/deps/libffi/libffi.map.in index f4e366eb60bc..6151f10cbdce 100644 --- a/deps/libffi/libffi.map.in +++ b/deps/libffi/libffi.map.in @@ -69,6 +69,15 @@ LIBFFI_CALL_PLAN_8.4 { ffi_call_plan_free; } LIBFFI_BASE_8.1; +/* ---------------------------------------------------------------------- + Call plan footprint query (ffi_call_plan_size). A fresh node because + LIBFFI_CALL_PLAN_8.4 has already shipped. + -------------------------------------------------------------------- */ +LIBFFI_CALL_PLAN_8.5 { + global: + ffi_call_plan_size; +} LIBFFI_CALL_PLAN_8.4; + #ifdef FFI_TARGET_HAS_COMPLEX_TYPE LIBFFI_COMPLEX_8.0 { global: diff --git a/deps/libffi/libtool-version b/deps/libffi/libtool-version index c5545eab8bf6..814c6e21d925 100644 --- a/deps/libffi/libtool-version +++ b/deps/libffi/libtool-version @@ -26,4 +26,4 @@ # release, then set age to 0. # # CURRENT:REVISION:AGE -12:1:4 +13:0:5 diff --git a/deps/libffi/src/aarch64/ffi.c b/deps/libffi/src/aarch64/ffi.c index 2e6a2ad2624c..1eb90dd565ad 100644 --- a/deps/libffi/src/aarch64/ffi.c +++ b/deps/libffi/src/aarch64/ffi.c @@ -92,6 +92,19 @@ ffi_clear_cache (void *start, void *end) #endif +/* Return the base-2 logarithm of N (N assumed to be a power of two). Used + to map a vector register width (8 or 16 bytes) onto the D-/Q-register + AARCH64_RET_* encoding. */ + +static int +intlog2 (int n) +{ + int level = 0; + while (n >>= 1) + ++level; + return level; +} + /* A subroutine of is_vfp_type. Given a structure type, return the type code of the first non-structure element. Recurse for structure elements. Return -1 if the structure is in fact empty, i.e. no nested elements. */ @@ -106,7 +119,8 @@ is_hfa0 (const ffi_type *ty) for (i = 0; elements[i]; ++i) { ret = elements[i]->type; - if (ret == FFI_TYPE_STRUCT || ret == FFI_TYPE_COMPLEX) + if (ret == FFI_TYPE_STRUCT || ret == FFI_TYPE_VECTOR + || ret == FFI_TYPE_COMPLEX) { ret = is_hfa0 (elements[i]); if (ret < 0) @@ -118,6 +132,33 @@ is_hfa0 (const ffi_type *ty) return ret; } +/* A subroutine of is_vfp_type. Return the size in bytes of the vector (SIMD) + member of TY, i.e. the width of a single Neon register slot, or 0 if TY + neither is nor contains a vector. For a bare vector this is its whole size; + for a homogeneous vector aggregate it is the size of one lane vector. */ + +static size_t +is_simd (const ffi_type *ty) +{ + ffi_type **elements; + int i; + + if (ty->type == FFI_TYPE_VECTOR) + return ty->size; + + elements = ty->elements; + if (elements != NULL) + for (i = 0; elements[i]; ++i) + { + int t = elements[i]->type; + if (t == FFI_TYPE_STRUCT || t == FFI_TYPE_COMPLEX + || t == FFI_TYPE_VECTOR) + return is_simd (elements[i]); + } + + return 0; +} + /* A subroutine of is_vfp_type. Given a structure type, return true if all of the non-structure elements are the same as CANDIDATE. */ @@ -131,7 +172,8 @@ is_hfa1 (const ffi_type *ty, int candidate) for (i = 0; elements[i]; ++i) { int t = elements[i]->type; - if (t == FFI_TYPE_STRUCT || t == FFI_TYPE_COMPLEX) + if (t == FFI_TYPE_STRUCT || t == FFI_TYPE_VECTOR + || t == FFI_TYPE_COMPLEX) { if (!is_hfa1 (elements[i], candidate)) return 0; @@ -156,7 +198,7 @@ is_vfp_type (const ffi_type *ty) { ffi_type **elements; int candidate, i; - size_t size, ele_count; + size_t size, ele_count, simd_size; /* Quickest tests first. */ candidate = ty->type; @@ -181,18 +223,24 @@ is_vfp_type (const ffi_type *ty) } return 0; case FFI_TYPE_STRUCT: + case FFI_TYPE_VECTOR: break; } - /* No HFA types are smaller than 4 bytes, or larger than 64 bytes. */ + /* No HFA/HVA types are smaller than 4 bytes, or larger than 64 bytes. */ size = ty->size; if (size < 4 || size > 64) return 0; - /* Find the type of the first non-structure member. */ + /* Determine the width of the vector (SIMD) member, if any: 0 for a plain + floating-point HFA, else the size in bytes of one Neon register slot. */ + simd_size = is_simd (ty); + + /* Find the type of the first non-aggregate member. */ elements = ty->elements; candidate = elements[0]->type; - if (candidate == FFI_TYPE_STRUCT || candidate == FFI_TYPE_COMPLEX) + if (candidate == FFI_TYPE_STRUCT || candidate == FFI_TYPE_VECTOR + || candidate == FFI_TYPE_COMPLEX) { for (i = 0; ; ++i) { @@ -202,6 +250,63 @@ is_vfp_type (const ffi_type *ty) } } + if (simd_size) + { + /* Vector or homogeneous vector aggregate (HVA). A single Neon slot is + at most 16 bytes (a Q register). A bare vector wider than 16 bytes + (e.g. a 32-byte double4) has no short-vector register class under + AAPCS64, so bail and let the generic composite path pass it by + reference / return it in memory -- matching what current compilers do. + The scalar lane type does not affect register selection (an integer + and a floating-point 16-byte vector both occupy one Q register), so, + unlike the floating-point HFA path below, CANDIDATE is used only to + confirm the lanes are homogeneous. */ + size_t reg_size = simd_size; + int num_registers; + int first_level_element_type; + + /* A Neon register slot is an S (4B), D (8B) or Q (16B). A lane narrower + than 4 bytes has no short-vector register class under AAPCS64 and would + map below AARCH64_RET_S4, making extend_hfa_type() branch before its + jump table; reject it and let the generic aggregate path handle it. */ + if (reg_size < 4 || reg_size > 16 || size % reg_size != 0) + return 0; + num_registers = (int) (size / reg_size); + if (num_registers > 4) + return 0; + + /* For an aggregate, every member must itself be a vector (or nested + vector aggregate) of the same register width: this rejects a struct + that mixes a bare scalar with a vector even when the scalar's type + matches the vector's lane type. A bare vector needs no such check -- + its lanes were validated when its layout was computed. */ + if (ty->type != FFI_TYPE_VECTOR) + for (i = 0; elements[i]; ++i) + if (is_simd (elements[i]) != reg_size) + return 0; + + /* Every lane must be the identical scalar type across the whole HVA + (this rejects, e.g., an aggregate mixing float and integer vectors). */ + for (i = 0; elements[i]; ++i) + { + int t = elements[i]->type; + if (t == FFI_TYPE_STRUCT || t == FFI_TYPE_VECTOR + || t == FFI_TYPE_COMPLEX) + { + if (!is_hfa1 (elements[i], candidate)) + return 0; + } + else if (t != candidate) + return 0; + } + + /* Reuse the AARCH64_RET_{S,D,Q}* codes, which are laid out as + (type * 4) + (4 - count) with FLOAT->S(4B), DOUBLE->D(8B), + LONGDOUBLE->Q(16B). Map the register width onto that type axis. */ + first_level_element_type = FFI_TYPE_FLOAT + intlog2 ((int) reg_size) - 2; + return first_level_element_type * 4 + (4 - num_registers); + } + /* If the first member is not a floating point type, it's not an HFA. Also quickly re-check the size of the structure. */ switch (candidate) @@ -614,6 +719,7 @@ ffi_prep_cif_machdep (ffi_cif *cif) case FFI_TYPE_DOUBLE: case FFI_TYPE_LONGDOUBLE: case FFI_TYPE_STRUCT: + case FFI_TYPE_VECTOR: case FFI_TYPE_COMPLEX: flags = is_vfp_type (rtype); if (flags == 0) @@ -802,6 +908,7 @@ ffi_call_int (ffi_cif *cif, void (*fn)(void), void *orig_rvalue, case FFI_TYPE_DOUBLE: case FFI_TYPE_LONGDOUBLE: case FFI_TYPE_STRUCT: + case FFI_TYPE_VECTOR: case FFI_TYPE_COMPLEX: { h = is_vfp_type (ty); @@ -1089,6 +1196,7 @@ ffi_closure_SYSV_inner (ffi_cif *cif, case FFI_TYPE_DOUBLE: case FFI_TYPE_LONGDOUBLE: case FFI_TYPE_STRUCT: + case FFI_TYPE_VECTOR: case FFI_TYPE_COMPLEX: h = is_vfp_type (ty); if (h) diff --git a/deps/libffi/src/aarch64/ffitarget.h b/deps/libffi/src/aarch64/ffitarget.h index 46e2687ae7fe..8ba86799e113 100644 --- a/deps/libffi/src/aarch64/ffitarget.h +++ b/deps/libffi/src/aarch64/ffitarget.h @@ -94,6 +94,10 @@ typedef enum ffi_abi #define FFI_TARGET_HAS_COMPLEX_TYPE #endif +/* AAPCS64 passes 8- and 16-byte vectors in V/Q registers and homogeneous + vector aggregates in consecutive V/Q registers; see is_vfp_type. */ +#define FFI_TARGET_HAS_VECTOR_TYPE + #define FFI_TARGET_HAS_INT128 1 #endif diff --git a/deps/libffi/src/debug.c b/deps/libffi/src/debug.c index 63321dc013cc..cf847f3b1107 100644 --- a/deps/libffi/src/debug.c +++ b/deps/libffi/src/debug.c @@ -54,7 +54,8 @@ void ffi_type_test(ffi_type *a, const char *file, int line) FFI_ASSERT_AT(a->type <= FFI_TYPE_LAST, file, line); FFI_ASSERT_AT(a->type == FFI_TYPE_VOID || a->size > 0, file, line); FFI_ASSERT_AT(a->type == FFI_TYPE_VOID || a->alignment > 0, file, line); - FFI_ASSERT_AT((a->type != FFI_TYPE_STRUCT && a->type != FFI_TYPE_COMPLEX) + FFI_ASSERT_AT((a->type != FFI_TYPE_STRUCT && a->type != FFI_TYPE_COMPLEX + && a->type != FFI_TYPE_VECTOR) || a->elements != NULL, file, line); FFI_ASSERT_AT(a->type != FFI_TYPE_COMPLEX || (a->elements != NULL diff --git a/deps/libffi/src/ia64/ia64_flags.h b/deps/libffi/src/ia64/ia64_flags.h index 9d652cef14ce..bfe102c7d86f 100644 --- a/deps/libffi/src/ia64/ia64_flags.h +++ b/deps/libffi/src/ia64/ia64_flags.h @@ -38,3 +38,14 @@ #define FFI_IA64_TYPE_HFA_FLOAT (FFI_TYPE_LAST + 2) #define FFI_IA64_TYPE_HFA_DOUBLE (FFI_TYPE_LAST + 3) #define FFI_IA64_TYPE_HFA_LDOUBLE (FFI_TYPE_LAST + 4) + +/* Tripwire: the .Lst_table / .Lld_table return-value jump tables in unix.S place + the FFI_IA64_TYPE_* pseudo-types (which are FFI_TYPE_LAST-relative) immediately + after the generic FFI_TYPE_* codes. Adding a new generic type bumps + FFI_TYPE_LAST, shifts those codes, and desyncs the tables -- silently + misdispatching small-struct/HFA returns. When this fires: add a matching slot + for the new type to both tables in unix.S, then bump FFI_IA64_TYPE_LAST. */ +#define FFI_IA64_TYPE_LAST FFI_TYPE_VECTOR +#if FFI_TYPE_LAST != FFI_IA64_TYPE_LAST +# error "new FFI_TYPE_* added: sync the unix.S jump tables and bump FFI_IA64_TYPE_LAST" +#endif diff --git a/deps/libffi/src/ia64/unix.S b/deps/libffi/src/ia64/unix.S index 04908368c3e2..b8e347169e2e 100644 --- a/deps/libffi/src/ia64/unix.S +++ b/deps/libffi/src/ia64/unix.S @@ -553,6 +553,9 @@ ffi_closure_unix: data8 @pcrel(.Lst_void) // FFI_TYPE_STRUCT data8 @pcrel(.Lst_int64) // FFI_TYPE_POINTER data8 @pcrel(.Lst_void) // FFI_TYPE_COMPLEX (not implemented) + data8 @pcrel(.Lst_void) // FFI_TYPE_UINT128 (not implemented) + data8 @pcrel(.Lst_void) // FFI_TYPE_SINT128 (not implemented) + data8 @pcrel(.Lst_void) // FFI_TYPE_VECTOR (rejected in ffi_prep_cif_core) data8 @pcrel(.Lst_small_struct) // FFI_IA64_TYPE_SMALL_STRUCT data8 @pcrel(.Lst_hfa_float) // FFI_IA64_TYPE_HFA_FLOAT data8 @pcrel(.Lst_hfa_double) // FFI_IA64_TYPE_HFA_DOUBLE @@ -575,6 +578,9 @@ ffi_closure_unix: data8 @pcrel(.Lld_void) // FFI_TYPE_STRUCT data8 @pcrel(.Lld_int) // FFI_TYPE_POINTER data8 @pcrel(.Lld_void) // FFI_TYPE_COMPLEX (not implemented) + data8 @pcrel(.Lld_void) // FFI_TYPE_UINT128 (not implemented) + data8 @pcrel(.Lld_void) // FFI_TYPE_SINT128 (not implemented) + data8 @pcrel(.Lld_void) // FFI_TYPE_VECTOR (rejected in ffi_prep_cif_core) data8 @pcrel(.Lld_small_struct) // FFI_IA64_TYPE_SMALL_STRUCT data8 @pcrel(.Lld_hfa_float) // FFI_IA64_TYPE_HFA_FLOAT data8 @pcrel(.Lld_hfa_double) // FFI_IA64_TYPE_HFA_DOUBLE diff --git a/deps/libffi/src/java_raw_api.c b/deps/libffi/src/java_raw_api.c index 114d3e47fcde..e0a02ef27432 100644 --- a/deps/libffi/src/java_raw_api.c +++ b/deps/libffi/src/java_raw_api.c @@ -58,7 +58,8 @@ ffi_java_raw_size (ffi_cif *cif) result += 2 * FFI_SIZEOF_JAVA_RAW; break; case FFI_TYPE_STRUCT: - /* No structure parameters in Java. */ + case FFI_TYPE_VECTOR: + /* No structure or vector parameters in Java. */ abort(); case FFI_TYPE_COMPLEX: /* Not supported yet. */ diff --git a/deps/libffi/src/pa/ffitarget.h b/deps/libffi/src/pa/ffitarget.h index f6f09975cfac..aeaacc167ef2 100644 --- a/deps/libffi/src/pa/ffitarget.h +++ b/deps/libffi/src/pa/ffitarget.h @@ -89,8 +89,13 @@ typedef enum ffi_abi { to the default case and is mapped to FFI_TYPE_INT, so cif->flags never exceeds FFI_TYPE_COMPLEX and the existing tables remain sufficient. Bump FFI_PA_TYPE_LAST to the current FFI_TYPE_LAST once you have confirmed any - newly added generic type is likewise handled (or the tables extended). */ -#define FFI_PA_TYPE_LAST FFI_TYPE_SINT128 + newly added generic type is likewise handled (or the tables extended). + + FFI_TYPE_VECTOR (18) is likewise not reached here: PA does not define + FFI_TARGET_HAS_VECTOR_TYPE, so ffi_prep_cif_core rejects any vector + signature with FFI_BAD_TYPEDEF before machdep runs. Bumping the tripwire + past it is therefore safe. */ +#define FFI_PA_TYPE_LAST FFI_TYPE_VECTOR /* Tripwire: when a new generic type is added FFI_TYPE_LAST changes and this fires, forcing a review of ffi_prep_cif_machdep and the linux.S / hpux32.S diff --git a/deps/libffi/src/powerpc/darwin_closure.S b/deps/libffi/src/powerpc/darwin_closure.S index 3121e6ac26d3..08cbe4bc1389 100644 --- a/deps/libffi/src/powerpc/darwin_closure.S +++ b/deps/libffi/src/powerpc/darwin_closure.S @@ -186,19 +186,17 @@ LCFI1: /* Make the call. */ bl BLCLS_HELP - /* r3 contains the rtype pointer... save it since we will need - it later. */ - sg r3,LINKAGE_SIZE(r1) ; ffi_type * result_type - lg r0,0(r3) ; size => r0 - lhz r3,FFI_TYPE_TYPE(r3) ; type => r3 - - /* The helper will have intercepted structure returns and inserted - the caller`s destination address for structs returned by ref. */ - - /* r3 contains the return type so use it to look up in a table - so we know how to deal with each type. */ - - addi r5,r1,(SAVE_SIZE-RESULT_BYTES) /* Otherwise, our return is here. */ + /* r3 now holds a small PPC_LD_* jump-table index (see the PPC_LD_* + defines in ffi_darwin.c), not an ffi_type* as this file previously + assumed: ffi_closure_helper_common cannot return both an ffi_type* + and the dispatch index through r3, so it returns the index. The + helper has already intercepted by-reference struct returns (writing + the result to the caller`s buffer and returning PPC_LD_NONE); for a + by-value struct return it returns PPC_LD_STRUCT and stashes cif->rtype + in the first parameter-save slot, which the PPC_LD_STRUCT fragment + below recovers. */ + + addi r5,r1,(SAVE_SIZE-RESULT_BYTES) /* Our return value is here. */ bl Lget_ret_type0_addr /* Get pointer to Lret_type0 into LR. */ mflr r4 /* Move to r4. */ slwi r3,r3,4 /* Now multiply return type by 16. */ @@ -218,43 +216,60 @@ LFE1: Lget_ret_type0_addr: blrl -/* case FFI_TYPE_VOID */ +/* The fragments below are indexed by the PPC_LD_* return code that + ffi_closure_helper_common handed back in r3, so their order must match the + PPC_LD_* values in ffi_darwin.c. Each is exactly 16 bytes (four + instructions), except the final PPC_LD_STRUCT fragment. */ + +/* case PPC_LD_NONE (void, or a struct returned by reference) */ Lret_type0: b Lfinish nop nop nop -/* case FFI_TYPE_INT */ +/* case PPC_LD_R3 (one GPR: int, pointer, and on ppc64 also 64-bit ints) */ Lret_type1: lg r3,0(r5) b Lfinish nop nop -/* case FFI_TYPE_FLOAT */ +/* case PPC_LD_R3R4 (two GPRs: the 32-bit ABI`s 64-bit integer) */ Lret_type2: +#if defined(__ppc64__) + lg r3,0(r5) + lg r4,8(r5) +#else + lwz r3,0(r5) + lwz r4,4(r5) +#endif + b Lfinish + nop + +/* case PPC_LD_F32 */ +Lret_type3: lfs f1,0(r5) b Lfinish nop nop -/* case FFI_TYPE_DOUBLE */ -Lret_type3: +/* case PPC_LD_F64 */ +Lret_type4: lfd f1,0(r5) b Lfinish nop nop -/* case FFI_TYPE_LONGDOUBLE */ -Lret_type4: +/* case PPC_LD_F128 (128-bit long double: two doubles) */ +Lret_type5: lfd f1,0(r5) lfd f2,8(r5) b Lfinish nop -/* case FFI_TYPE_UINT8 */ -Lret_type5: +/* case PPC_LD_U8 */ +Lret_type6: #if defined(__ppc64__) lbz r3,7(r5) #else @@ -264,8 +279,8 @@ Lret_type5: nop nop -/* case FFI_TYPE_SINT8 */ -Lret_type6: +/* case PPC_LD_S8 */ +Lret_type7: #if defined(__ppc64__) lbz r3,7(r5) #else @@ -275,8 +290,8 @@ Lret_type6: b Lfinish nop -/* case FFI_TYPE_UINT16 */ -Lret_type7: +/* case PPC_LD_U16 */ +Lret_type8: #if defined(__ppc64__) lhz r3,6(r5) #else @@ -286,8 +301,8 @@ Lret_type7: nop nop -/* case FFI_TYPE_SINT16 */ -Lret_type8: +/* case PPC_LD_S16 */ +Lret_type9: #if defined(__ppc64__) lha r3,6(r5) #else @@ -297,77 +312,43 @@ Lret_type8: nop nop -/* case FFI_TYPE_UINT32 */ -Lret_type9: #if defined(__ppc64__) - lwz r3,4(r5) -#else - lwz r3,0(r5) -#endif - b Lfinish - nop - nop - -/* case FFI_TYPE_SINT32 */ +/* case PPC_LD_U32 (ppc64 only; the 32-bit ABI aliases U32 to PPC_LD_R3) */ Lret_type10: -#if defined(__ppc64__) lwz r3,4(r5) -#else - lwz r3,0(r5) -#endif b Lfinish nop nop -/* case FFI_TYPE_UINT64 */ +/* case PPC_LD_S32 (ppc64 only; the 32-bit ABI aliases S32 to PPC_LD_R3) */ Lret_type11: -#if defined(__ppc64__) - lg r3,0(r5) - b Lfinish - nop -#else - lwz r3,0(r5) - lwz r4,4(r5) + lwa r3,4(r5) b Lfinish -#endif nop - -/* case FFI_TYPE_SINT64 */ -Lret_type12: -#if defined(__ppc64__) - lg r3,0(r5) - b Lfinish nop -#else - lwz r3,0(r5) - lwz r4,4(r5) - b Lfinish #endif - nop -/* case FFI_TYPE_STRUCT */ -Lret_type13: +/* case PPC_LD_STRUCT (a by-value struct return). This is the final, + variable-length fragment, so it need not be padded to 16 bytes. The helper + stashed cif->rtype in the first parameter-save slot (see ffi_darwin.c), + because the small dispatch index in r3 left no room for it. */ +Lret_type_struct: + lg r6,PARENT_PARM_BASE(r1) ; cif->rtype + sg r6,LINKAGE_SIZE(r1) ; where the struct code below expects it + lg r0,0(r6) ; size => r0 #if defined(__ppc64__) lg r3,0(r5) ; we need at least this... cmpi 0,r0,4 bgt Lstructend ; not a special small case b Lsmallstruct ; see if we need more. #else - cmpwi 0,r0,4 - bgt Lfinish ; not by value - lg r3,0(r5) + lg r3,0(r5) ; a <=4-byte struct, returned in r3 b Lfinish #endif -/* case FFI_TYPE_POINTER */ -Lret_type14: - lg r3,0(r5) - b Lfinish - nop - nop #if defined(__ppc64__) Lsmallstruct: - beq Lfour ; continuation of Lret13. + beq Lfour ; continuation of Lret_type_struct. cmpi 0,r0,3 beq Lfinish ; don`t adjust this - can`t be any floats here... srdi r3,r3,48 diff --git a/deps/libffi/src/powerpc/ffi_darwin.c b/deps/libffi/src/powerpc/ffi_darwin.c index 01e2a43701d7..64449c38e156 100644 --- a/deps/libffi/src/powerpc/ffi_darwin.c +++ b/deps/libffi/src/powerpc/ffi_darwin.c @@ -60,11 +60,13 @@ struct ffi_aix_trampoline_struct { # define PPC_LD_S32 PPC_LD_R3 # define PPC_LD_PTR PPC_LD_R3 # define PPC_LD_I64 PPC_LD_R3R4 +# define PPC_LD_STRUCT 10 #else # define PPC_LD_U32 10 # define PPC_LD_S32 11 # define PPC_LD_PTR PPC_LD_R3 # define PPC_LD_I64 PPC_LD_R3 +# define PPC_LD_STRUCT 12 #endif extern void ffi_closure_ASM (void); @@ -1260,6 +1262,13 @@ ffi_closure_helper_common (ffi_cif* cif, long i, avn; ffi_dblfl * end_pfr = pfr + NUM_FPR_ARG_REGISTERS; unsigned size_al; + int struct_ret_by_value = 0; + /* When a struct is returned by value, ffi_closure_ASM's jump-table + dispatch carries only a small integer return code (see PPC_LD_* above), + with no room for cif->rtype. We hand cif->rtype back in the first + parameter-save slot -- which is dead by the time we return -- for the + PPC_LD_STRUCT fragment in darwin_closure.S to recover. */ + unsigned long * pgr0 = pgr; #if defined(POWERPC_DARWIN64) unsigned fpsused = 0; #endif @@ -1275,12 +1284,16 @@ ffi_closure_helper_common (ffi_cif* cif, rvalue = (void *) *pgr; pgr++; } + else + struct_ret_by_value = 1; #elif defined(DARWIN_PPC) if (cif->rtype->size > 4) { rvalue = (void *) *pgr; pgr++; } + else + struct_ret_by_value = 1; #else /* assume we return by ref. */ rvalue = (void *) *pgr; pgr++; @@ -1480,7 +1493,17 @@ ffi_closure_helper_common (ffi_cif* cif, switch (cif->rtype->type) { case FFI_TYPE_VOID: + return PPC_LD_NONE; case FFI_TYPE_STRUCT: + /* A by-reference struct return needs nothing further here: the result + was written straight to the caller's buffer. A by-value struct + return is loaded into registers by darwin_closure.S, which needs + cif->rtype -- hand it back in the first parameter-save slot. */ + if (struct_ret_by_value) + { + *pgr0 = (unsigned long) cif->rtype; + return PPC_LD_STRUCT; + } return PPC_LD_NONE; case FFI_TYPE_FLOAT: return PPC_LD_F32; diff --git a/deps/libffi/src/powerpc/ffi_linux64.c b/deps/libffi/src/powerpc/ffi_linux64.c index b1f1468ed5f8..e92f88c46973 100644 --- a/deps/libffi/src/powerpc/ffi_linux64.c +++ b/deps/libffi/src/powerpc/ffi_linux64.c @@ -107,8 +107,13 @@ discover_homogeneous_aggregate (ffi_abi abi, unsigned int inner_elnum = 0; unsigned int inner = discover_homogeneous_aggregate (abi, t->elements[0], &inner_elnum); - if (inner == FFI_TYPE_FLOAT || inner == FFI_TYPE_DOUBLE) + if (inner == FFI_TYPE_FLOAT || inner == FFI_TYPE_DOUBLE + || inner == FFI_TYPE_LONGDOUBLE) { + /* A _Complex of an FP base counts as two of that base: an + FP-HFA struct member. For IBM-128 long double each half is + itself two FPRs (inner_elnum == 2), so a _Complex long double + contributes four FPRs. */ *elnum = 2 * inner_elnum; return inner; } @@ -257,11 +262,17 @@ ffi_prep_cif_linux64_core (ffi_cif *cif) goto homogeneous; #if FFI_TYPE_LONGDOUBLE != FFI_TYPE_DOUBLE case FFI_TYPE_LONGDOUBLE: - /* Only the 64-bit long double case is wired up; IBM-128 and - IEEE-binary128 _Complex are left as a follow-up. */ - if ((cif->abi & (FFI_LINUX_LONG_DOUBLE_128 - | FFI_LINUX_LONG_DOUBLE_IEEE128)) != 0) - return FFI_BAD_TYPEDEF; + if ((cif->abi & FFI_LINUX_LONG_DOUBLE_IEEE128) != 0) + { + /* IEEE-128 _Complex long double: real in v2, imag in v3. + Return via the vector-homogeneous small-struct path. */ + flags |= FLAG_RETURNS_SMST | FLAG_RETURNS_VEC; + break; + } + /* IBM-128 _Complex long double is returned like a homogeneous + aggregate of doubles: real in f1:f2, imag in f3:f4. (For a + 64-bit long double this reduces to the FFI_TYPE_DOUBLE case, + real in f1 and imag in f2.) */ flags |= FLAG_RETURNS_SMST; rtype = FFI_TYPE_DOUBLE; goto homogeneous; @@ -393,11 +404,21 @@ ffi_prep_cif_linux64_core (ffi_cif *cif) break; #if FFI_TYPE_LONGDOUBLE != FFI_TYPE_DOUBLE case FFI_TYPE_LONGDOUBLE: - if ((cif->abi & (FFI_LINUX_LONG_DOUBLE_128 - | FFI_LINUX_LONG_DOUBLE_IEEE128)) != 0) - return FFI_BAD_TYPEDEF; - fparg_count += 2; - intarg_count += 2; + if ((cif->abi & FFI_LINUX_LONG_DOUBLE_IEEE128) != 0) + { + /* Two IEEE-128 halves: each occupies a vector register plus + two GPR shadow doublewords, the pair 16-byte aligned. */ + vecarg_count += 2; + intarg_count = (intarg_count + 1) & ~0x1; + intarg_count += 4; + if (vecarg_count > NUM_VEC_ARG_REGISTERS64) + flags |= FLAG_ARG_NEEDS_PSAVE; + break; + } + /* IBM-128: each half is a pair of FPRs, and each FPR half + consumes a GPR shadow doubleword -- four of each in total. */ + fparg_count += 4; + intarg_count += 4; if (fparg_count > NUM_FPR_ARG_REGISTERS64) flags |= FLAG_ARG_NEEDS_PSAVE; break; @@ -755,10 +776,51 @@ ffi_prep_args64 (extended_cif *ecif, unsigned long *const stack) case FFI_TYPE_COMPLEX: elt = (*ptr)->elements[0]->type; #if FFI_TYPE_LONGDOUBLE != FFI_TYPE_DOUBLE - /* 64-bit long double is equivalent to double; the IBM-128 and - IEEE-binary128 variants were rejected in prep_cif. */ + if (elt == FFI_TYPE_LONGDOUBLE + && (ecif->cif->abi & FFI_LINUX_LONG_DOUBLE_IEEE128) != 0) + { + /* IEEE-128 _Complex long double: each half goes in its own + vector register (or the parameter save area), 16-byte + aligned, consuming two GPR shadow doublewords. */ + float128 *cval = (float128 *) *p_argv.v; + unsigned int j; + for (j = 0; j < 2; j++) + { + next_arg.p = FFI_ALIGN (next_arg.p, 16); + if (next_arg.ul == gpr_end.ul) + next_arg.ul = rest.ul; + if (vecarg_count < NUM_VEC_ARG_REGISTERS64 && i < nfixedargs) + memcpy (vec_base.f128++, cval + j, sizeof (float128)); + else + memcpy (next_arg.f128, cval + j, sizeof (float128)); + if (++next_arg.f128 == gpr_end.f128) + next_arg.f128 = rest.f128; + vecarg_count++; + } + FFI_ASSERT (flags & FLAG_VEC_ARGUMENTS); + break; + } if (elt == FFI_TYPE_LONGDOUBLE) - elt = FFI_TYPE_DOUBLE; + { + /* IBM-128 _Complex long double: four doubles (real hi/lo, + imag hi/lo) into consecutive FPRs, each with a GPR shadow + doubleword. */ + double *cval = (double *) *p_argv.v; + unsigned int j; + for (j = 0; j < 4; j++) + { + double_tmp = cval[j]; + if (fparg_count < NUM_FPR_ARG_REGISTERS64 && i < nfixedargs) + *fpr_base.d++ = double_tmp; + else + *next_arg.d = double_tmp; + if (++next_arg.ul == gpr_end.ul) + next_arg.ul = rest.ul; + fparg_count++; + } + FFI_ASSERT (flags & FLAG_FP_ARGUMENTS); + break; + } #endif if (elt == FFI_TYPE_FLOAT) { @@ -1336,8 +1398,45 @@ ffi_closure_helper_LINUX64 (ffi_cif *cif, unsigned int j; elt = arg_types[i]->elements[0]->type; #if FFI_TYPE_LONGDOUBLE != FFI_TYPE_DOUBLE + if (elt == FFI_TYPE_LONGDOUBLE + && (cif->abi & FFI_LINUX_LONG_DOUBLE_IEEE128) != 0) + { + /* IEEE-128: each half arrives in a vector register (or the + 16-byte-aligned parameter save area) with two GPR shadow + doublewords. */ + float128 *cval = alloca (2 * sizeof (float128)); + if (((unsigned long) pst & 0xF) != 0) + ++pst; + for (j = 0; j < 2; j++) + { + if (pvec < end_pvec && i < nfixedargs) + memcpy (&cval[j], pvec++, sizeof (float128)); + else + memcpy (&cval[j], pst, sizeof (float128)); + pst += 2; + } + avalue[i] = cval; + break; + } if (elt == FFI_TYPE_LONGDOUBLE) - elt = FFI_TYPE_DOUBLE; + { + /* IBM-128: four doubles, each in an FPR (or one GPR shadow + doubleword) -- real hi/lo then imag hi/lo. */ + double *cval = alloca (4 * sizeof (double)); + for (j = 0; j < 4; j++) + { + if (pfr < end_pfr && i < nfixedargs) + { + cval[j] = pfr->d; + pfr++; + } + else + cval[j] = *(double *) pst; + pst++; + } + avalue[i] = cval; + break; + } #endif if (elt == FFI_TYPE_FLOAT) { @@ -1448,7 +1547,13 @@ ffi_closure_helper_LINUX64 (ffi_cif *cif, int inner = cif->rtype->elements[0]->type; #if FFI_TYPE_LONGDOUBLE != FFI_TYPE_DOUBLE if (inner == FFI_TYPE_LONGDOUBLE) - inner = FFI_TYPE_DOUBLE; + { + /* IEEE-128 _Complex long double returns in v2:v3; IBM-128 in + f1:f2 (real) and f3:f4 (imag), i.e. as a double HFA. */ + if ((cif->abi & FFI_LINUX_LONG_DOUBLE_IEEE128) != 0) + return PPC64_LD_VECTOR_HOMOG; + inner = FFI_TYPE_DOUBLE; + } #endif if (inner == FFI_TYPE_FLOAT) return PPC64_LD_FLOAT_HOMOG; diff --git a/deps/libffi/src/powerpc/linux64_closure.S b/deps/libffi/src/powerpc/linux64_closure.S index 405b2cfc47a1..3071bec0d22a 100644 --- a/deps/libffi/src/powerpc/linux64_closure.S +++ b/deps/libffi/src/powerpc/linux64_closure.S @@ -345,6 +345,21 @@ E PPC64_LD_STRUCT_3 lwz %r3, RETVAL+4(%r1) srd %r3, %r3, 8 epilogue + +E PPC64_LD_STRUCT_5 + ld %r3, RETVAL+0(%r1) + srdi %r3, %r3, 24 + epilogue + +E PPC64_LD_STRUCT_6 + ld %r3, RETVAL+0(%r1) + srdi %r3, %r3, 16 + epilogue + +E PPC64_LD_STRUCT_7 + ld %r3, RETVAL+0(%r1) + srdi %r3, %r3, 8 + epilogue #endif .Lmoredouble: diff --git a/deps/libffi/src/prep_cif.c b/deps/libffi/src/prep_cif.c index 1836270d1a9c..8a448ebbf81f 100644 --- a/deps/libffi/src/prep_cif.c +++ b/deps/libffi/src/prep_cif.c @@ -32,6 +32,72 @@ #define STACK_ARG_SIZE(x) FFI_ALIGN(x, FFI_SIZEOF_ARG) +/* Compute the machine-independent layout of a vector (SIMD) type. + + A vector is described exactly like a struct -- arg->elements is a + NULL-terminated array of pointers -- but every element must point to the + SAME fundamental scalar type, and the count is the number of lanes. The + caller leaves arg->size and arg->alignment as zero; libffi derives them: + + size = lane_size * lane_count, rounded UP to the next power of two + (matching Clang's ext_vector_type storage, e.g. 3 x float + -> 16; GCC's vector_size already requires power-of-two totals + so the rule is identical there); + alignment = min(size, 16). + + Only float, double and the fixed-width integer scalars (UINT8..SINT64) are + valid lane types. Anything else -- a heterogeneous element list, an + aggregate lane, long double, or a zero-length vector -- is FFI_BAD_TYPEDEF. */ + +static ffi_status +initialize_vector (ffi_type *arg) +{ + ffi_type **ptr = arg->elements; + ffi_type *elem; + size_t count = 0; + size_t total, p2; + + if (UNLIKELY (ptr == NULL || *ptr == NULL)) + return FFI_BAD_TYPEDEF; + + elem = *ptr; + switch (elem->type) + { + case FFI_TYPE_FLOAT: + case FFI_TYPE_DOUBLE: + case FFI_TYPE_UINT8: + case FFI_TYPE_SINT8: + case FFI_TYPE_UINT16: + case FFI_TYPE_SINT16: + case FFI_TYPE_UINT32: + case FFI_TYPE_SINT32: + case FFI_TYPE_UINT64: + case FFI_TYPE_SINT64: + break; + default: + return FFI_BAD_TYPEDEF; + } + + /* Every lane must be the identical scalar type. */ + for (; *ptr != NULL; ptr++) + { + if ((*ptr)->type != elem->type || (*ptr)->size != elem->size) + return FFI_BAD_TYPEDEF; + count++; + } + + if (UNLIKELY (count < 1 || elem->size == 0)) + return FFI_BAD_TYPEDEF; + + total = elem->size * count; + for (p2 = 1; p2 < total; p2 <<= 1) + ; + + arg->size = p2; + arg->alignment = p2 < 16 ? p2 : 16; + return FFI_OK; +} + /* Perform machine independent initialization of aggregate type specifications. */ @@ -42,6 +108,9 @@ static ffi_status initialize_aggregate(ffi_type *arg, size_t *offsets) if (UNLIKELY(arg == NULL || arg->elements == NULL)) return FFI_BAD_TYPEDEF; + if (arg->type == FFI_TYPE_VECTOR) + return initialize_vector (arg); + arg->size = 0; arg->alignment = 0; @@ -92,6 +161,28 @@ static ffi_status initialize_aggregate(ffi_type *arg, size_t *offsets) return FFI_OK; } +#ifndef FFI_TARGET_HAS_VECTOR_TYPE +/* Recursively test whether TY is, or contains, a vector (SIMD) type. Ports + that do not define FFI_TARGET_HAS_VECTOR_TYPE cannot marshal vectors, so + ffi_prep_cif_core rejects any signature that mentions one (directly or + nested inside a struct) with FFI_BAD_TYPEDEF rather than aborting. */ +static int +ffi_type_contains_vector (ffi_type *ty) +{ + ffi_type **p; + + if (ty == NULL) + return 0; + if (ty->type == FFI_TYPE_VECTOR) + return 1; + if (ty->type == FFI_TYPE_STRUCT && ty->elements != NULL) + for (p = ty->elements; *p != NULL; p++) + if (ffi_type_contains_vector (*p)) + return 1; + return 0; +} +#endif /* !FFI_TARGET_HAS_VECTOR_TYPE */ + #ifndef __CRIS__ /* The CRIS ABI specifies structure elements to have byte alignment only, so it completely overrides this functions, @@ -129,6 +220,15 @@ ffi_status FFI_HIDDEN ffi_prep_cif_core(ffi_cif *cif, ffi_abi abi, cif->nargs = ntotalargs; cif->rtype = rtype; +#ifndef FFI_TARGET_HAS_VECTOR_TYPE + /* Vector (SIMD) types are only marshalled on ports that opt in. */ + if (ffi_type_contains_vector (rtype)) + return FFI_BAD_TYPEDEF; + for (i = 0; i < ntotalargs; i++) + if (ffi_type_contains_vector (atypes[i])) + return FFI_BAD_TYPEDEF; +#endif + cif->flags = 0; #if (defined(_M_ARM64) || defined(__aarch64__)) && defined(_WIN32) cif->is_variadic = isvariadic; @@ -152,7 +252,8 @@ ffi_status FFI_HIDDEN ffi_prep_cif_core(ffi_cif *cif, ffi_abi abi, /* x86, x86-64 and s390 stack space allocation is handled in prep_machdep. */ #if !defined FFI_TARGET_SPECIFIC_STACK_SPACE_ALLOCATION /* Make space for the return structure pointer */ - if (cif->rtype->type == FFI_TYPE_STRUCT + if ((cif->rtype->type == FFI_TYPE_STRUCT + || cif->rtype->type == FFI_TYPE_VECTOR) #ifdef TILE && (cif->rtype->size > 10 * FFI_SIZEOF_ARG) #endif @@ -316,4 +417,11 @@ ffi_call_plan_free (ffi_call_plan *plan) free (plan); } +size_t +ffi_call_plan_size (ffi_call_plan *plan) +{ + /* The generic plan is a bare handle; there is no separate move-list. */ + return plan != NULL ? sizeof (struct ffi_call_plan) : 0; +} + #endif /* generic ffi_call_plan fallback */ diff --git a/deps/libffi/src/raw_api.c b/deps/libffi/src/raw_api.c index be156116cb0d..670d56d948a3 100644 --- a/deps/libffi/src/raw_api.c +++ b/deps/libffi/src/raw_api.c @@ -42,7 +42,7 @@ ffi_raw_size (ffi_cif *cif) for (i = cif->nargs-1; i >= 0; i--, at++) { #if !FFI_NO_STRUCTS - if ((*at)->type == FFI_TYPE_STRUCT) + if ((*at)->type == FFI_TYPE_STRUCT || (*at)->type == FFI_TYPE_VECTOR) result += FFI_ALIGN (sizeof (void*), FFI_SIZEOF_ARG); else #endif @@ -82,8 +82,9 @@ ffi_raw_to_ptrarray (ffi_cif *cif, ffi_raw *raw, void **args) break; #endif -#if !FFI_NO_STRUCTS +#if !FFI_NO_STRUCTS case FFI_TYPE_STRUCT: + case FFI_TYPE_VECTOR: *args = (raw++)->ptr; break; #endif @@ -110,7 +111,7 @@ ffi_raw_to_ptrarray (ffi_cif *cif, ffi_raw *raw, void **args) for (i = 0; i < cif->nargs; i++, tp++, args++) { #if !FFI_NO_STRUCTS - if ((*tp)->type == FFI_TYPE_STRUCT) + if ((*tp)->type == FFI_TYPE_STRUCT || (*tp)->type == FFI_TYPE_VECTOR) { *args = (raw++)->ptr; } @@ -172,6 +173,7 @@ ffi_ptrarray_to_raw (ffi_cif *cif, void **args, ffi_raw *raw) #if !FFI_NO_STRUCTS case FFI_TYPE_STRUCT: + case FFI_TYPE_VECTOR: (raw++)->ptr = *args; break; #endif diff --git a/deps/libffi/src/tramp.c b/deps/libffi/src/tramp.c index 525f81547156..a04188858af6 100644 --- a/deps/libffi/src/tramp.c +++ b/deps/libffi/src/tramp.c @@ -417,9 +417,19 @@ ffi_tramp_init (void) &tramp_globals.map_size); tramp_globals.ntramp = tramp_globals.map_size / tramp_globals.size; + /* + * The trampoline code table is a single, fixed-size mapping. If the + * system page size is larger than that mapping, the static trampoline + * mechanism cannot be used. Both values are invariant for the life of + * the process, so cache the FAILED verdict rather than re-running the + * whole initialization on every allocation. + */ page_size = sysconf (_SC_PAGESIZE); if (page_size >= 0 && (size_t)page_size > tramp_globals.map_size) - return 0; + { + tramp_globals.status = TRAMP_GLOBALS_FAILED; + return 0; + } if (ffi_tramp_init_os ()) { diff --git a/deps/libffi/src/x86/ffi.c b/deps/libffi/src/x86/ffi.c index 27f17b0c8849..a953891362d5 100644 --- a/deps/libffi/src/x86/ffi.c +++ b/deps/libffi/src/x86/ffi.c @@ -118,7 +118,7 @@ ffi_prep_cif_machdep(ffi_cif *cif) break; case FFI_TYPE_STRUCT: { -#if defined(X86_WIN32) || defined(X86_DARWIN) +#if defined(X86_WIN32) || defined(X86_DARWIN) || defined(X86_FREEBSD) size_t size = cif->rtype->size; if (size == 1) flags = X86_RET_STRUCT_1B; diff --git a/deps/libffi/src/x86/ffi64.c b/deps/libffi/src/x86/ffi64.c index c24db38c4364..c2c78f3fbbfb 100644 --- a/deps/libffi/src/x86/ffi64.c +++ b/deps/libffi/src/x86/ffi64.c @@ -330,6 +330,25 @@ classify_argument (ffi_type *type, enum x86_64_reg_class classes[], } return words; } + case FFI_TYPE_VECTOR: + /* A Short Vector occupies SSE registers: an 8-byte vector is a single + SSE eightbyte; a 16-byte vector is one %xmm register (SSE + SSEUP). + Wider vectors would need %ymm/%zmm handling this port does not + implement; classify them as memory here and reject them outright in + ffi_prep_cif_machdep so the caller gets FFI_BAD_TYPEDEF, not a + silently wrong in-memory pass. */ + if (type->size == 8) + { + classes[0] = X86_64_SSE_CLASS; + return 1; + } + else if (type->size == 16) + { + classes[0] = X86_64_SSE_CLASS; + classes[1] = X86_64_SSEUP_CLASS; + return 2; + } + return 0; case FFI_TYPE_COMPLEX: { ffi_type *inner = type->elements[0]; @@ -533,6 +552,16 @@ ffi_prep_cif_machdep (ffi_cif *cif) } } break; + case FFI_TYPE_VECTOR: + /* An 8-byte vector returns in the low half of %xmm0; a 16-byte vector + fills %xmm0 (SSE + SSEUP). Wider vectors are unsupported here. */ + if (rtype_size == 8) + flags = UNIX64_RET_XMM64; + else if (rtype_size == 16) + flags = UNIX64_RET_XMM128; + else + return FFI_BAD_TYPEDEF; + break; case FFI_TYPE_COMPLEX: switch (rtype->elements[0]->type) { @@ -577,6 +606,15 @@ ffi_prep_cif_machdep (ffi_cif *cif) return FFI_BAD_TYPEDEF; } + /* Reject vectors wider than 16 bytes as arguments: correct %ymm/%zmm + passing needs unix64.S register-save changes that are out of scope for + this port, and classify_argument would otherwise silently treat them as + an in-memory aggregate. */ + for (i = 0, avn = cif->nargs; i < avn; i++) + if (cif->arg_types[i]->type == FFI_TYPE_VECTOR + && cif->arg_types[i]->size > 16) + return FFI_BAD_TYPEDEF; + /* Go over all arguments and determine the way they should be passed. If it's in a register and there is space for it, let that be so. If not, add it's size to the stack byte count. */ @@ -782,6 +820,7 @@ typedef struct unsigned fast; /* nonzero -> lean trampoline eligible */ unsigned retcode; /* UNIX64_RET_* (low byte of flags) for the store */ int thunk_n; /* >=0 -> ffi_gp_thunks[thunk_n], else -1 */ + unsigned alloc_bytes; /* malloc'd size, reported by ffi_call_plan_size */ ffi_move moves[]; } ffi_plan; @@ -828,7 +867,7 @@ build_plan (ffi_cif *cif) unsigned i, avn = cif->nargs; enum x86_64_reg_class classes[MAX_CLASSES]; unsigned nm, gprcount, ssecount; - size_t argp_off; + size_t argp_off, nbytes; ffi_plan *plan; int all_gp64 = 1; /* every arg is exactly one 64-bit GP move? */ @@ -848,9 +887,11 @@ build_plan (ffi_cif *cif) } /* One self-contained allocation: header + moves, released with plain free(). */ - plan = malloc (sizeof (ffi_plan) + sizeof (ffi_move) * (2 * avn + 1)); + nbytes = sizeof (ffi_plan) + sizeof (ffi_move) * (2 * avn + 1); + plan = malloc (nbytes); if (plan == NULL) return NULL; + plan->alloc_bytes = (unsigned) nbytes; nm = gprcount = ssecount = 0; argp_off = 0; @@ -1070,6 +1111,17 @@ ffi_call_plan_free (ffi_call_plan *plan) } } +size_t +ffi_call_plan_size (ffi_call_plan *plan) +{ + if (plan == NULL) + return 0; + /* The move-list carries its own size; a signature with no fast path owns + nothing beyond the handle. */ + return sizeof (struct ffi_call_plan) + + (plan->fast != NULL ? plan->fast->alloc_bytes : 0); +} + extern void ffi_call_efi64(ffi_cif *cif, void (*fn)(void), void *rvalue, void **avalue); #endif diff --git a/deps/libffi/src/x86/ffitarget.h b/deps/libffi/src/x86/ffitarget.h index d702235f90fe..eaf6a910a4f5 100644 --- a/deps/libffi/src/x86/ffitarget.h +++ b/deps/libffi/src/x86/ffitarget.h @@ -58,6 +58,13 @@ #define FFI_TARGET_HAS_INT128 #endif +/* The System V x86-64 psABI passes 8- and 16-byte vectors in SSE registers; + this is implemented by the ffi64.c (FFI_UNIX64) backend only. 32-bit x86 + and the Windows x86-64 backend (ffiw64.c) do not marshal vectors. */ +#if defined(X86_64) && !defined(X86_WIN64) +#define FFI_TARGET_HAS_VECTOR_TYPE +#endif + /* ---- Generic type definitions ----------------------------------------- */ #ifndef LIBFFI_ASM @@ -138,6 +145,18 @@ typedef enum ffi_abi { #define FFI_TYPE_SMALL_STRUCT_4B (FFI_TYPE_LAST + 3) #define FFI_TYPE_MS_STRUCT (FFI_TYPE_LAST + 4) +/* Tripwire: the win64.S / win64_intel.S return-value jump tables use one + 8-byte slot per code value and place the FFI_TYPE_SMALL_STRUCT_* pseudo-types + (which are FFI_TYPE_LAST-relative) immediately after the generic FFI_TYPE_* + codes. Adding a new generic type bumps FFI_TYPE_LAST, shifts those codes, + and opens a gap in the tables that silently misaligns small-struct returns. + When this fires: add a matching E() slot for the new type in both win64.S + and win64_intel.S, then bump FFI_X86_TYPE_LAST to match. */ +#define FFI_X86_TYPE_LAST FFI_TYPE_VECTOR +#if FFI_TYPE_LAST != FFI_X86_TYPE_LAST +# error "new FFI_TYPE_* added: sync the win64.S/win64_intel.S jump tables and bump FFI_X86_TYPE_LAST" +#endif + #if defined (X86_64) || defined(X86_WIN64) \ || (defined (__x86_64__) && defined (X86_DARWIN)) /* 4 bytes of ENDBR64 + 7 bytes of LEA + 6 bytes of JMP + 7 bytes of NOP diff --git a/deps/libffi/src/x86/win64.S b/deps/libffi/src/x86/win64.S index 185f0a3048fb..f23a5fa29e8e 100644 --- a/deps/libffi/src/x86/win64.S +++ b/deps/libffi/src/x86/win64.S @@ -151,6 +151,11 @@ E(0b, FFI_TYPE_UINT128) E(0b, FFI_TYPE_SINT128) movdqu %xmm0, (%r8) epilogue +/* Win64 does not marshal vectors (ffi_prep_cif_core rejects them), but the + FFI_TYPE_SMALL_STRUCT_* codes are FFI_TYPE_LAST-relative, so this slot must + exist to keep the table contiguous and the small-struct entries aligned. */ +E(0b, FFI_TYPE_VECTOR) + call PLT(C(abort)) E(0b, FFI_TYPE_SMALL_STRUCT_1B) movb %al, (%r8) epilogue diff --git a/deps/libffi/src/x86/win64_intel.S b/deps/libffi/src/x86/win64_intel.S index e9eff00da3ce..807f5b3e98f7 100644 --- a/deps/libffi/src/x86/win64_intel.S +++ b/deps/libffi/src/x86/win64_intel.S @@ -152,6 +152,11 @@ E(0b, FFI_TYPE_UINT128) E(0b, FFI_TYPE_SINT128) movdqu xmmword ptr [r8], xmm0 epilogue +/* Win64 does not marshal vectors (ffi_prep_cif_core rejects them), but the + FFI_TYPE_SMALL_STRUCT_* codes are FFI_TYPE_LAST-relative, so this slot must + exist to keep the table contiguous and the small-struct entries aligned. */ +E(0b, FFI_TYPE_VECTOR) + call PLT(C(abort)) E(0b, FFI_TYPE_SMALL_STRUCT_1B) mov byte ptr [r8], al ; movb %al, (%r8) epilogue diff --git a/deps/libffi/testsuite/Makefile.am b/deps/libffi/testsuite/Makefile.am index c14a880959d8..702461d02418 100644 --- a/deps/libffi/testsuite/Makefile.am +++ b/deps/libffi/testsuite/Makefile.am @@ -13,15 +13,17 @@ EXTRA_DIST = config/default.exp emscripten/build.sh emscripten/conftest.py \ libffi.bhaible/alignof.h libffi.bhaible/bhaible.exp libffi.bhaible/test-call.c \ libffi.bhaible/test-callback.c libffi.bhaible/testcases.c libffi.call/align_mixed.c \ libffi.call/align_stdcall.c libffi.call/bpo_38748.c libffi.call/call.exp \ + libffi.call/closure_thiscall_fastcall_pop.c \ libffi.call/err_bad_typedef.c libffi.call/ffitest.h libffi.call/float.c \ libffi.call/float1.c libffi.call/float2.c libffi.call/float3.c \ libffi.call/float4.c libffi.call/float_va.c libffi.call/i128-1.c \ libffi.call/large_struct_by_value.c libffi.call/many.c \ - libffi.call/many2.c libffi.call/many_double.c libffi.call/many_mixed.c \ + libffi.call/many2.c libffi.call/many_double.c \ + libffi.call/many_large_structs.c libffi.call/many_mixed.c \ libffi.call/many_small_structs.c \ libffi.call/negint.c libffi.call/offsets.c libffi.call/overread.c \ libffi.call/plan.c libffi.call/plan_mixed.c libffi.call/plan_spill.c \ - libffi.call/plan_struct.c libffi.call/plan_var.c \ + libffi.call/plan_struct.c libffi.call/plan_size.c libffi.call/plan_var.c \ libffi.call/pr1172638.c libffi.call/promotion.c libffi.call/pyobjc_tc.c libffi.call/return_dbl.c \ libffi.call/return_dbl1.c libffi.call/return_dbl2.c libffi.call/return_fl.c \ libffi.call/return_fl1.c libffi.call/return_fl2.c libffi.call/return_fl3.c \ @@ -90,4 +92,10 @@ EXTRA_DIST = config/default.exp emscripten/build.sh emscripten/conftest.py \ libffi.complex/return_complex_float.c libffi.complex/return_complex_longdouble.c libffi.go/aa-direct.c \ libffi.go/closure1.c libffi.go/ffitest.h libffi.go/go.exp \ libffi.go/static-chain.h Makefile.am Makefile.in \ - libffi.threads/ffitest.h libffi.threads/threads.exp libffi.threads/tsan.c + libffi.threads/ffitest.h libffi.threads/threads.exp libffi.threads/tsan.c \ + libffi.vector/vector.exp libffi.vector/ffitest.h libffi.vector/vector.h \ + libffi.vector/vector_float32x4.c libffi.vector/vector_float32x2.c \ + libffi.vector/vector_double2.c libffi.vector/vector_int32x4.c \ + libffi.vector/vector_args_spill.c libffi.vector/vector_vec3.c \ + libffi.vector/vector_double4.c libffi.vector/vector_hva.c \ + libffi.vector/cls_vector.c libffi.vector/vector_validate.c diff --git a/deps/libffi/testsuite/Makefile.in b/deps/libffi/testsuite/Makefile.in index 1b29b90d3633..30b417735d5e 100644 --- a/deps/libffi/testsuite/Makefile.in +++ b/deps/libffi/testsuite/Makefile.in @@ -301,15 +301,17 @@ EXTRA_DIST = config/default.exp emscripten/build.sh emscripten/conftest.py \ libffi.bhaible/alignof.h libffi.bhaible/bhaible.exp libffi.bhaible/test-call.c \ libffi.bhaible/test-callback.c libffi.bhaible/testcases.c libffi.call/align_mixed.c \ libffi.call/align_stdcall.c libffi.call/bpo_38748.c libffi.call/call.exp \ + libffi.call/closure_thiscall_fastcall_pop.c \ libffi.call/err_bad_typedef.c libffi.call/ffitest.h libffi.call/float.c \ libffi.call/float1.c libffi.call/float2.c libffi.call/float3.c \ libffi.call/float4.c libffi.call/float_va.c libffi.call/i128-1.c \ libffi.call/large_struct_by_value.c libffi.call/many.c \ - libffi.call/many2.c libffi.call/many_double.c libffi.call/many_mixed.c \ + libffi.call/many2.c libffi.call/many_double.c \ + libffi.call/many_large_structs.c libffi.call/many_mixed.c \ libffi.call/many_small_structs.c \ libffi.call/negint.c libffi.call/offsets.c libffi.call/overread.c \ libffi.call/plan.c libffi.call/plan_mixed.c libffi.call/plan_spill.c \ - libffi.call/plan_struct.c libffi.call/plan_var.c \ + libffi.call/plan_struct.c libffi.call/plan_size.c libffi.call/plan_var.c \ libffi.call/pr1172638.c libffi.call/promotion.c libffi.call/pyobjc_tc.c libffi.call/return_dbl.c \ libffi.call/return_dbl1.c libffi.call/return_dbl2.c libffi.call/return_fl.c \ libffi.call/return_fl1.c libffi.call/return_fl2.c libffi.call/return_fl3.c \ @@ -378,7 +380,13 @@ EXTRA_DIST = config/default.exp emscripten/build.sh emscripten/conftest.py \ libffi.complex/return_complex_float.c libffi.complex/return_complex_longdouble.c libffi.go/aa-direct.c \ libffi.go/closure1.c libffi.go/ffitest.h libffi.go/go.exp \ libffi.go/static-chain.h Makefile.am Makefile.in \ - libffi.threads/ffitest.h libffi.threads/threads.exp libffi.threads/tsan.c + libffi.threads/ffitest.h libffi.threads/threads.exp libffi.threads/tsan.c \ + libffi.vector/vector.exp libffi.vector/ffitest.h libffi.vector/vector.h \ + libffi.vector/vector_float32x4.c libffi.vector/vector_float32x2.c \ + libffi.vector/vector_double2.c libffi.vector/vector_int32x4.c \ + libffi.vector/vector_args_spill.c libffi.vector/vector_vec3.c \ + libffi.vector/vector_double4.c libffi.vector/vector_hva.c \ + libffi.vector/cls_vector.c libffi.vector/vector_validate.c all: all-am diff --git a/deps/libffi/testsuite/libffi.call/closure_thiscall_fastcall_pop.c b/deps/libffi/testsuite/libffi.call/closure_thiscall_fastcall_pop.c new file mode 100644 index 000000000000..9cc0b091943f --- /dev/null +++ b/deps/libffi/testsuite/libffi.call/closure_thiscall_fastcall_pop.c @@ -0,0 +1,131 @@ +/* Area: closure, ffi_prep_closure_loc + Purpose: Check i386 THISCALL/FASTCALL closures pop the stack correctly. + Limitations: i386 + GNU inline asm only; a no-op elsewhere. + PR: none. + Originator: i386 closure stack-pop accounting regression. + + THISCALL and FASTCALL are callee-clean: the closure must remove its + stack-resident arguments on return (ret $n). When a 64-bit integer or + a struct argument is placed on the stack, the closure return path used + to compute the pop as cif->bytes - narg_reg*4 with narg_reg force-bumped + to 2, discounting register slots that were never used and under-popping + the stack. A caller that relies on callee cleanup is then left with the + argument bytes where its return address should be. + + This test invokes the generated closure through a minimal callee-clean + call site and checks that ESP is balanced across the call (delta 0). + Without the fix the delta is 8 (FASTCALL uint64) or 4 (THISCALL). */ + +/* { dg-do run } */ +#include "ffitest.h" + +#if defined(__i386__) && defined(__GNUC__) && !defined(__APPLE__) + +static uint64_t received; +static int ran; + +static void +cb (ffi_cif *cif, void *resp, void **args, void *userdata) +{ + (void) cif; (void) resp; (void) userdata; + received = *(uint64_t *) args[cif->nargs - 1]; + ran++; +} + +/* Push an 8-byte stack argument, load ECX (the thiscall "this" register, + ignored by the fastcall callee), call the closure, and return how many + bytes the callee under-popped (0 == it popped exactly what was pushed). + + Every operand is read into a register up front, while ESP is still at + its incoming value, so nothing is referenced through an ESP-relative + memory operand after we start moving ESP (which would otherwise read a + stale slot, on clang at -O2 in particular). The stack is then 16-byte + aligned at the call as the i386 psABI requires, so the -O2-built closure + body may use aligned SSE without faulting; the alignment cancels out of + the delta. ESP is restored to its exact incoming value before the delta + is stored, so a wrong pop cannot corrupt our frame. Not using EBX keeps + this compatible with -fPIC; the delta is returned via memory so no free + register is needed for it. */ +static int +esp_delta (void *code, uint64_t stackarg, unsigned ecxv) +{ + unsigned delta; + unsigned lo = (unsigned) stackarg; + unsigned hi = (unsigned) (stackarg >> 32); + __asm__ volatile ( + "movl %[lo], %%eax\n\t" /* stash all operands in registers */ + "movl %[hi], %%edx\n\t" /* before ESP moves */ + "movl %[code], %%edi\n\t" + "movl %[ecxv], %%ecx\n\t" /* thiscall 'this' */ + "movl %%esp, %%esi\n\t" /* remember the real esp */ + "andl $-16, %%esp\n\t" /* 16-byte align, then bias by the */ + "subl $8, %%esp\n\t" /* 8 arg bytes so 'call' is 0 mod 16 */ + "pushl %%edx\n\t" /* high dword */ + "pushl %%eax\n\t" /* low dword */ + "calll *%%edi\n\t" + "movl %%esi, %%eax\n\t" /* recompute esp just before the */ + "andl $-16, %%eax\n\t" /* pushes... */ + "subl $8, %%eax\n\t" + "subl %%esp, %%eax\n\t" /* eax = under-popped byte count */ + "movl %%esi, %%esp\n\t" /* restore before touching memory */ + "movl %%eax, %[delta]\n\t" + : [delta] "=m" (delta) + : [lo] "m" (lo), [hi] "m" (hi), [code] "m" (code), [ecxv] "m" (ecxv) + : "memory", "cc", "eax", "ecx", "edx", "esi", "edi"); + return (int) delta; +} + +static int +check_abi (ffi_abi abi, unsigned nargs, ffi_type **atypes, unsigned ecx) +{ + ffi_cif cif; + ffi_closure *closure; + void *code; + int delta; + + closure = ffi_closure_alloc (sizeof (ffi_closure), &code); + CHECK (closure != NULL); + CHECK (ffi_prep_cif (&cif, abi, nargs, &ffi_type_void, atypes) == FFI_OK); + CHECK (ffi_prep_closure_loc (closure, &cif, cb, NULL, code) == FFI_OK); + + ran = 0; + received = 0; + delta = esp_delta (code, 0x1122334455667788ULL, ecx); + + CHECK (ran == 1); + CHECK (received == 0x1122334455667788ULL); + ffi_closure_free (closure); + return delta; +} + +int +main (void) +{ + ffi_type *fastcall_args[1] = { &ffi_type_uint64 }; + ffi_type *thiscall_args[2] = { &ffi_type_pointer, &ffi_type_uint64 }; + int d; + + /* FASTCALL void cb(uint64_t): the uint64 is stack-resident; pop must be 8. */ + d = check_abi (FFI_FASTCALL, 1, fastcall_args, 0); + printf ("FASTCALL uint64 esp delta: %d\n", d); + CHECK (d == 0); + + /* THISCALL void cb(void*, uint64_t): 'this' in ECX, uint64 on the stack; + pop must be 8 (not 4). */ + d = check_abi (FFI_THISCALL, 2, thiscall_args, 0xdeadbeef); + printf ("THISCALL this+uint64 esp delta: %d\n", d); + CHECK (d == 0); + + exit (0); +} + +#else + +int +main (void) +{ + /* Not an i386 GNU target: nothing to check here. */ + exit (0); +} + +#endif diff --git a/deps/libffi/testsuite/libffi.call/many_large_structs.c b/deps/libffi/testsuite/libffi.call/many_large_structs.c new file mode 100644 index 000000000000..9f766a9113c6 --- /dev/null +++ b/deps/libffi/testsuite/libffi.call/many_large_structs.c @@ -0,0 +1,88 @@ +/* Area: ffi_call + Purpose: Pass many large by-value structs on AArch64. + Limitations: none. + PR: none. + Originator: AArch64 large-struct stack accounting regression. + + Regression test: on AArch64, composites larger than 16 bytes are passed + by invisible reference. ffi_call copies each payload into the argument + slab (growing down from the top) and, once X0-X7 are exhausted, also + spills the by-ref pointer into the same slab (the NSAA, growing up). + The generic prep_cif budget in cif->bytes only charged the payload copy, + not the 8-byte pointer slot, so with enough large structs the two regions + collided and a later payload copy overwrote an already-spilled pointer, + leaving the callee with a corrupt pointer for a by-value argument. + Passing sixteen 32-byte (non-HFA) structs by value -- eight more than the + argument registers -- must marshal every argument intact. */ + +/* { dg-do run } */ +#include "ffitest.h" + +#define NARGS 16 +#define SSIZE 32 + +typedef struct { unsigned char b[SSIZE]; } big_struct; + +/* Sum every byte of every argument. A corrupted by-ref pointer makes the + callee read the wrong memory, so the sum no longer matches. */ +static int ABI_ATTR +sum_bytes (big_struct s0, big_struct s1, big_struct s2, big_struct s3, + big_struct s4, big_struct s5, big_struct s6, big_struct s7, + big_struct s8, big_struct s9, big_struct s10, big_struct s11, + big_struct s12, big_struct s13, big_struct s14, big_struct s15) +{ + big_struct *all[NARGS]; + int i, j, sum = 0; + + all[0] = &s0; all[1] = &s1; all[2] = &s2; all[3] = &s3; + all[4] = &s4; all[5] = &s5; all[6] = &s6; all[7] = &s7; + all[8] = &s8; all[9] = &s9; all[10] = &s10; all[11] = &s11; + all[12] = &s12; all[13] = &s13; all[14] = &s14; all[15] = &s15; + + for (i = 0; i < NARGS; i++) + for (j = 0; j < SSIZE; j++) + sum += all[i]->b[j]; + + return sum; +} + +int main (void) +{ + ffi_cif cif; + ffi_type *args[NARGS]; + void *values[NARGS]; + ffi_type bs_type; + ffi_type *bs_elements[SSIZE + 1]; + big_struct in[NARGS]; + ffi_arg result = 0; + int i, j, expected = 0; + + bs_type.size = 0; + bs_type.alignment = 0; + bs_type.type = FFI_TYPE_STRUCT; + for (i = 0; i < SSIZE; i++) + bs_elements[i] = &ffi_type_uchar; + bs_elements[SSIZE] = NULL; + bs_type.elements = bs_elements; + + /* Fill struct i with the distinct byte value (i + 1) so any pointer + mix-up between arguments changes the total. */ + for (i = 0; i < NARGS; i++) + { + for (j = 0; j < SSIZE; j++) + { + in[i].b[j] = (unsigned char) (i + 1); + expected += (i + 1); + } + args[i] = &bs_type; + values[i] = &in[i]; + } + + CHECK(ffi_prep_cif(&cif, ABI_NUM, NARGS, &ffi_type_sint, args) == FFI_OK); + + ffi_call(&cif, FFI_FN(sum_bytes), &result, values); + + CHECK((int) result == expected); + + exit(0); +} diff --git a/deps/libffi/testsuite/libffi.call/plan_size.c b/deps/libffi/testsuite/libffi.call/plan_size.c new file mode 100644 index 000000000000..b8398fbee991 --- /dev/null +++ b/deps/libffi/testsuite/libffi.call/plan_size.c @@ -0,0 +1,77 @@ +/* Area: ffi_call_plan_size + Purpose: Check that a plan reports its own allocation size, that the + size is stable across invocations, and that a NULL plan has + no footprint. + Limitations: The exact byte count is implementation defined, so this only + checks the invariants callers may rely on. + PR: none. + Originator: ffi_call_plan tests */ + +/* { dg-do run } */ +#include "ffitest.h" + +static uint64_t gp2(uint64_t a, uint64_t b) +{ + return a + b * 2; +} + +static uint64_t gp6(uint64_t a, uint64_t b, uint64_t c, + uint64_t d, uint64_t e, uint64_t f) +{ + return a + b * 2 + c * 3 + d * 4 + e * 5 + f * 6; +} + +int main (void) +{ + ffi_cif cif2, cif6; + ffi_type *args[6]; + void *values[6]; + ffi_call_plan *plan2, *plan6; + size_t size2, size6; + uint64_t a[6], r; + int i; + + for (i = 0; i < 6; i++) + { + args[i] = &ffi_type_uint64; + a[i] = (uint64_t) (i + 1); + values[i] = &a[i]; + } + + CHECK(ffi_prep_cif(&cif2, FFI_DEFAULT_ABI, 2, &ffi_type_uint64, args) + == FFI_OK); + CHECK(ffi_prep_cif(&cif6, FFI_DEFAULT_ABI, 6, &ffi_type_uint64, args) + == FFI_OK); + + /* A NULL plan has no footprint, mirroring ffi_call_plan_free(NULL). */ + CHECK(ffi_call_plan_size(NULL) == 0); + + plan2 = ffi_call_plan_alloc(&cif2); + CHECK(plan2 != NULL); + plan6 = ffi_call_plan_alloc(&cif6); + CHECK(plan6 != NULL); + + size2 = ffi_call_plan_size(plan2); + size6 = ffi_call_plan_size(plan6); + + /* Every plan owns at least its handle, and a wider signature never needs + less memory than a narrower one of the same shape. Targets without a + fast path report the same constant for both. */ + CHECK(size2 > 0); + CHECK(size6 >= size2); + + /* The plan is immutable, so querying it must not disturb invocation and + the reported size must not drift across calls. */ + ffi_call_plan_invoke(plan6, FFI_FN(gp6), &r, values); + CHECK(r == gp6(a[0], a[1], a[2], a[3], a[4], a[5])); + CHECK(ffi_call_plan_size(plan6) == size6); + + ffi_call_plan_invoke(plan2, FFI_FN(gp2), &r, values); + CHECK(r == gp2(a[0], a[1])); + CHECK(ffi_call_plan_size(plan2) == size2); + + ffi_call_plan_free(plan2); + ffi_call_plan_free(plan6); + + exit(0); +} diff --git a/deps/libffi/testsuite/libffi.vector/cls_vector.c b/deps/libffi/testsuite/libffi.vector/cls_vector.c new file mode 100644 index 000000000000..18d8806b51b5 --- /dev/null +++ b/deps/libffi/testsuite/libffi.vector/cls_vector.c @@ -0,0 +1,67 @@ +/* Area: closure_call + Purpose: A closure that receives two vector arguments (and a scalar) and + returns a vector. Exercises the closure argument-extraction and + vector return paths. + Limitations: none. + PR: none. + Originator: libffi vector support. */ + +/* { dg-do run } */ + +#include "vector.h" + +typedef float f32x4 __attribute__((vector_size (16))); + +static void +cls_vector_fn (ffi_cif *cif __UNUSED__, void *resp, void **args, + void *userdata __UNUSED__) +{ + f32x4 a = *(f32x4 *) args[0]; + f32x4 b = *(f32x4 *) args[1]; + int scale = *(int *) args[2]; + f32x4 *r = (f32x4 *) resp; + + *r = (a + b) * (float) scale; +} + +typedef f32x4 (*cls_vector_t) (f32x4, f32x4, int); + +int +main (void) +{ + ffi_cif cif; + void *code; + ffi_closure *pcl = ffi_closure_alloc (sizeof (ffi_closure), &code); + ffi_type vec_type; + ffi_type *vec_elems[5]; + ffi_type *arg_types[3]; + f32x4 a = { 1, 2, 3, 4 }; + f32x4 b = { 10, 20, 30, 40 }; + f32x4 res; + int scale = 2; + int i; + + CHECK (pcl != NULL); + + make_vector_type (&vec_type, vec_elems, &ffi_type_float, 4); + + arg_types[0] = &vec_type; + arg_types[1] = &vec_type; + arg_types[2] = &ffi_type_sint; + + CHECK (ffi_prep_cif (&cif, FFI_DEFAULT_ABI, 3, &vec_type, arg_types) + == FFI_OK); + CHECK (ffi_prep_closure_loc (pcl, &cif, cls_vector_fn, NULL, code) + == FFI_OK); + + res = ((cls_vector_t) code) (a, b, scale); + + for (i = 0; i < 4; i++) + { + float want = (a[i] + b[i]) * (float) scale; + printf ("res[%d] = %g (want %g)\n", i, (double) res[i], (double) want); + CHECK (res[i] == want); + } + + exit (0); +} diff --git a/deps/libffi/testsuite/libffi.vector/ffitest.h b/deps/libffi/testsuite/libffi.vector/ffitest.h new file mode 100644 index 000000000000..d27d362d6a6e --- /dev/null +++ b/deps/libffi/testsuite/libffi.vector/ffitest.h @@ -0,0 +1 @@ +#include "../libffi.call/ffitest.h" diff --git a/deps/libffi/testsuite/libffi.vector/vector.exp b/deps/libffi/testsuite/libffi.vector/vector.exp new file mode 100644 index 000000000000..a76957ee4d33 --- /dev/null +++ b/deps/libffi/testsuite/libffi.vector/vector.exp @@ -0,0 +1,59 @@ +# Copyright (C) 2026 Free Software Foundation, Inc. + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; see the file COPYING3. If not see +# . + +dg-init +libffi-init + +global srcdir subdir + +# The tests are written with the GCC/Clang vector extension +# (__attribute__ ((vector_size (N)))). A target port can support +# FFI_TYPE_VECTOR at the ABI level while the compiler under test (e.g. +# MSVC) cannot compile that syntax, so probe the compiler with an actual +# compilation, not just a preprocessor check. +proc libffi_vector_syntax_test { } { + set src "vecprobe[pid].c" + set obj "vecprobe[pid].o" + + set f [open $src "w"] + puts $f "typedef float probe_v4 __attribute__ ((vector_size (16)));" + puts $f "probe_v4 probe_var;" + puts $f "int main (void) { return 0; }" + close $f + + set lines [libffi_target_compile $src $obj object ""] + file delete $src + file delete $obj + + return [string match "" $lines] +} + +set tlist [lsort [glob -nocomplain -- $srcdir/$subdir/*.{c,cc}]] + +if { [libffi_feature_test "#ifdef FFI_TARGET_HAS_VECTOR_TYPE"] + && [libffi_vector_syntax_test] } { + run-many-tests $tlist "" +} else { + foreach test $tlist { + unsupported "$test" + } +} + +dg-finish + +# Local Variables: +# tcl-indent-level:4 +# End: diff --git a/deps/libffi/testsuite/libffi.vector/vector.h b/deps/libffi/testsuite/libffi.vector/vector.h new file mode 100644 index 000000000000..7baf832d37e4 --- /dev/null +++ b/deps/libffi/testsuite/libffi.vector/vector.h @@ -0,0 +1,32 @@ +/* -*-c-*- */ +/* Shared helpers for the libffi vector (SIMD) tests. + + Vectors are built with the portable GCC/Clang spelling + __attribute__((vector_size (N))) so the tests compile on both compilers. + A vector ffi_type is described exactly like a struct, except every element + points at the SAME scalar ffi_type and the count is the lane count; the + caller leaves size and alignment at zero and libffi computes them. */ + +#ifndef LIBFFI_VECTOR_H +#define LIBFFI_VECTOR_H + +#include "ffitest.h" + +/* Build (into the caller-provided ELEMS array of length COUNT + 1 and the + ffi_type object TY) a vector type descriptor of COUNT lanes of scalar type + ELEM. ELEMS must have room for COUNT + 1 pointers (NULL terminator). */ +static inline void +make_vector_type (ffi_type *ty, ffi_type **elems, ffi_type *elem, + unsigned count) +{ + unsigned i; + for (i = 0; i < count; i++) + elems[i] = elem; + elems[count] = NULL; + ty->size = 0; + ty->alignment = 0; + ty->type = FFI_TYPE_VECTOR; + ty->elements = elems; +} + +#endif /* LIBFFI_VECTOR_H */ diff --git a/deps/libffi/testsuite/libffi.vector/vector_args_spill.c b/deps/libffi/testsuite/libffi.vector/vector_args_spill.c new file mode 100644 index 000000000000..dec6ab2e62af --- /dev/null +++ b/deps/libffi/testsuite/libffi.vector/vector_args_spill.c @@ -0,0 +1,85 @@ +/* Area: ffi_call + Purpose: Pass many vector arguments interleaved with scalars, enough to + exhaust the vector argument registers and spill onto the stack. + Limitations: none. + PR: none. + Originator: libffi vector support. */ + +/* { dg-do run } */ + +#include "vector.h" + +typedef float f32x4 __attribute__((vector_size (16))); + +/* Ten vectors exceeds the 8 vector argument registers on both AArch64 and + x86-64, so v8/v9 are passed on the stack. The scalars are interleaved to + make sure the two register files advance independently. */ +static float +mix (int i0, f32x4 v0, f32x4 v1, double d0, f32x4 v2, f32x4 v3, + f32x4 v4, int i1, f32x4 v5, f32x4 v6, f32x4 v7, double d1, + f32x4 v8, f32x4 v9) +{ + float acc = 0; + acc += 1 * v0[0] + v0[3]; + acc += 2 * v1[0] + v1[3]; + acc += 3 * v2[0] + v2[3]; + acc += 4 * v3[0] + v3[3]; + acc += 5 * v4[0] + v4[3]; + acc += 6 * v5[0] + v5[3]; + acc += 7 * v6[0] + v6[3]; + acc += 8 * v7[0] + v7[3]; + acc += 9 * v8[0] + v8[3]; + acc += 10 * v9[0] + v9[3]; + acc += i0 + i1 + (float) d0 + (float) d1; + return acc; +} + +int +main (void) +{ + ffi_cif cif; + ffi_type vec_type; + ffi_type *vec_elems[5]; + ffi_type *args[14]; + void *values[14]; + f32x4 v[10]; + int i0 = 100, i1 = 7; + double d0 = 3.5, d1 = 0.25; + float r, ref; + unsigned k; + + make_vector_type (&vec_type, vec_elems, &ffi_type_float, 4); + + for (k = 0; k < 10; k++) + { + f32x4 t = { (float) (k + 1), 0, 0, (float) (100 + k) }; + v[k] = t; + } + + args[0] = &ffi_type_sint; values[0] = &i0; + args[1] = &vec_type; values[1] = &v[0]; + args[2] = &vec_type; values[2] = &v[1]; + args[3] = &ffi_type_double; values[3] = &d0; + args[4] = &vec_type; values[4] = &v[2]; + args[5] = &vec_type; values[5] = &v[3]; + args[6] = &vec_type; values[6] = &v[4]; + args[7] = &ffi_type_sint; values[7] = &i1; + args[8] = &vec_type; values[8] = &v[5]; + args[9] = &vec_type; values[9] = &v[6]; + args[10] = &vec_type; values[10] = &v[7]; + args[11] = &ffi_type_double; values[11] = &d1; + args[12] = &vec_type; values[12] = &v[8]; + args[13] = &vec_type; values[13] = &v[9]; + + CHECK (ffi_prep_cif (&cif, FFI_DEFAULT_ABI, 14, &ffi_type_float, args) + == FFI_OK); + + ffi_call (&cif, FFI_FN (mix), &r, values); + + ref = mix (i0, v[0], v[1], d0, v[2], v[3], v[4], i1, v[5], v[6], v[7], + d1, v[8], v[9]); + printf ("r = %g (want %g)\n", (double) r, (double) ref); + CHECK (r == ref); + + exit (0); +} diff --git a/deps/libffi/testsuite/libffi.vector/vector_double2.c b/deps/libffi/testsuite/libffi.vector/vector_double2.c new file mode 100644 index 000000000000..dd45878b5afe --- /dev/null +++ b/deps/libffi/testsuite/libffi.vector/vector_double2.c @@ -0,0 +1,53 @@ +/* Area: ffi_call + Purpose: Pass and return a 16-byte double2 vector (single Q/SSE reg). + Limitations: none. + PR: none. + Originator: libffi vector support. */ + +/* { dg-do run } */ + +#include "vector.h" + +typedef double d2 __attribute__((vector_size (16))); + +static d2 +add_d2 (d2 a, d2 b) +{ + return a + b; +} + +int +main (void) +{ + ffi_cif cif; + ffi_type vec_type; + ffi_type *vec_elems[3]; + ffi_type *args[2]; + void *values[2]; + d2 a = { 1.5, 2.5 }; + d2 b = { 10.0, 20.0 }; + d2 r, ref; + int i; + + make_vector_type (&vec_type, vec_elems, &ffi_type_double, 2); + + args[0] = &vec_type; + args[1] = &vec_type; + values[0] = &a; + values[1] = &b; + + CHECK (ffi_prep_cif (&cif, FFI_DEFAULT_ABI, 2, &vec_type, args) == FFI_OK); + CHECK (vec_type.size == 16); + CHECK (vec_type.alignment == 16); + + ffi_call (&cif, FFI_FN (add_d2), &r, values); + + ref = add_d2 (a, b); + for (i = 0; i < 2; i++) + { + printf ("r[%d] = %g (want %g)\n", i, r[i], ref[i]); + CHECK (r[i] == ref[i]); + } + + exit (0); +} diff --git a/deps/libffi/testsuite/libffi.vector/vector_double4.c b/deps/libffi/testsuite/libffi.vector/vector_double4.c new file mode 100644 index 000000000000..9f4473935929 --- /dev/null +++ b/deps/libffi/testsuite/libffi.vector/vector_double4.c @@ -0,0 +1,81 @@ +/* Area: ffi_call + Purpose: A 32-byte double4 vector. On AArch64 a bare vector wider than + 16 bytes is passed by reference and returned in memory (no + short-vector register class), so the call must round-trip. On + x86-64 wider-than-16-byte vectors are not implemented, so + ffi_prep_cif must report FFI_BAD_TYPEDEF. + Limitations: none. + PR: none. + Originator: libffi vector support. */ + +/* { dg-do run } */ + +#include "vector.h" + +typedef double d4 __attribute__((vector_size (32))); + +/* Only called on ports that can actually marshal a 32-byte vector. */ +static d4 add_d4 (d4 a, d4 b) __UNUSED__; + +static d4 +add_d4 (d4 a, d4 b) +{ + return a + b; +} + +int +main (void) +{ + ffi_cif cif; + ffi_type vec_type; + ffi_type *vec_elems[5]; + ffi_type *args[2]; + + make_vector_type (&vec_type, vec_elems, &ffi_type_double, 4); + args[0] = &vec_type; + args[1] = &vec_type; + +#if defined(__aarch64__) || defined(_M_ARM64) + { + void *values[2]; + d4 a = { 1, 2, 3, 4 }; + d4 b = { 10, 20, 30, 40 }; + d4 r, ref; + int i; + + values[0] = &a; + values[1] = &b; + + CHECK (ffi_prep_cif (&cif, FFI_DEFAULT_ABI, 2, &vec_type, args) == FFI_OK); + CHECK (vec_type.size == 32); + CHECK (vec_type.alignment == 16); + + ffi_call (&cif, FFI_FN (add_d4), &r, values); + + ref = add_d4 (a, b); + for (i = 0; i < 4; i++) + { + printf ("r[%d] = %g (want %g)\n", i, r[i], ref[i]); + CHECK (r[i] == ref[i]); + } + } +#else + { + /* x86-64 (and any other opted-in port without >16B support): the >16-byte + vector must be rejected, both as a return type and as an argument. */ + ffi_status s_ret, s_arg; + + s_ret = ffi_prep_cif (&cif, FFI_DEFAULT_ABI, 0, &vec_type, NULL); + printf ("32-byte vector return: status %d (want %d = FFI_BAD_TYPEDEF)\n", + s_ret, FFI_BAD_TYPEDEF); + CHECK (s_ret == FFI_BAD_TYPEDEF); + + s_arg = ffi_prep_cif (&cif, FFI_DEFAULT_ABI, 1, &ffi_type_void, args); + printf ("32-byte vector argument: status %d (want %d = FFI_BAD_TYPEDEF)\n", + s_arg, FFI_BAD_TYPEDEF); + CHECK (s_arg == FFI_BAD_TYPEDEF); + } +#endif + + exit (0); +} diff --git a/deps/libffi/testsuite/libffi.vector/vector_float32x2.c b/deps/libffi/testsuite/libffi.vector/vector_float32x2.c new file mode 100644 index 000000000000..f613687a2988 --- /dev/null +++ b/deps/libffi/testsuite/libffi.vector/vector_float32x2.c @@ -0,0 +1,53 @@ +/* Area: ffi_call + Purpose: Pass and return an 8-byte float32x2 vector (single D/SSE reg). + Limitations: none. + PR: none. + Originator: libffi vector support. */ + +/* { dg-do run } */ + +#include "vector.h" + +typedef float f32x2 __attribute__((vector_size (8))); + +static f32x2 +add_f32x2 (f32x2 a, f32x2 b) +{ + return a + b; +} + +int +main (void) +{ + ffi_cif cif; + ffi_type vec_type; + ffi_type *vec_elems[3]; + ffi_type *args[2]; + void *values[2]; + f32x2 a = { 3, 4 }; + f32x2 b = { 5, 6 }; + f32x2 r, ref; + int i; + + make_vector_type (&vec_type, vec_elems, &ffi_type_float, 2); + + args[0] = &vec_type; + args[1] = &vec_type; + values[0] = &a; + values[1] = &b; + + CHECK (ffi_prep_cif (&cif, FFI_DEFAULT_ABI, 2, &vec_type, args) == FFI_OK); + CHECK (vec_type.size == 8); + CHECK (vec_type.alignment == 8); + + ffi_call (&cif, FFI_FN (add_f32x2), &r, values); + + ref = add_f32x2 (a, b); + for (i = 0; i < 2; i++) + { + printf ("r[%d] = %g (want %g)\n", i, (double) r[i], (double) ref[i]); + CHECK (r[i] == ref[i]); + } + + exit (0); +} diff --git a/deps/libffi/testsuite/libffi.vector/vector_float32x4.c b/deps/libffi/testsuite/libffi.vector/vector_float32x4.c new file mode 100644 index 000000000000..971814aaf66c --- /dev/null +++ b/deps/libffi/testsuite/libffi.vector/vector_float32x4.c @@ -0,0 +1,54 @@ +/* Area: ffi_call + Purpose: Pass and return a 16-byte float32x4 vector (the vec4 shape of + libffi/libffi#773). + Limitations: none. + PR: libffi/libffi#773. + Originator: libffi vector support. */ + +/* { dg-do run } */ + +#include "vector.h" + +typedef float f32x4 __attribute__((vector_size (16))); + +static f32x4 +add_f32x4 (f32x4 a, f32x4 b) +{ + return a + b; +} + +int +main (void) +{ + ffi_cif cif; + ffi_type vec_type; + ffi_type *vec_elems[5]; + ffi_type *args[2]; + void *values[2]; + f32x4 a = { 1, 2, 3, 4 }; + f32x4 b = { 10, 20, 30, 40 }; + f32x4 r, ref; + int i; + + make_vector_type (&vec_type, vec_elems, &ffi_type_float, 4); + + args[0] = &vec_type; + args[1] = &vec_type; + values[0] = &a; + values[1] = &b; + + CHECK (ffi_prep_cif (&cif, FFI_DEFAULT_ABI, 2, &vec_type, args) == FFI_OK); + CHECK (vec_type.size == 16); + CHECK (vec_type.alignment == 16); + + ffi_call (&cif, FFI_FN (add_f32x4), &r, values); + + ref = add_f32x4 (a, b); + for (i = 0; i < 4; i++) + { + printf ("r[%d] = %g (want %g)\n", i, (double) r[i], (double) ref[i]); + CHECK (r[i] == ref[i]); + } + + exit (0); +} diff --git a/deps/libffi/testsuite/libffi.vector/vector_hva.c b/deps/libffi/testsuite/libffi.vector/vector_hva.c new file mode 100644 index 000000000000..5f80da07dfde --- /dev/null +++ b/deps/libffi/testsuite/libffi.vector/vector_hva.c @@ -0,0 +1,74 @@ +/* Area: ffi_call + Purpose: Pass and return a homogeneous vector aggregate: a struct of two + identical 16-byte vectors. On AArch64 this is an HVA carried in + a pair of Q registers; on x86-64 the existing SSE struct + classification handles it (four SSE eightbytes). Both round-trip. + Limitations: none. + PR: none. + Originator: libffi vector support. */ + +/* { dg-do run } */ + +#include "vector.h" + +typedef float f32x4 __attribute__((vector_size (16))); + +struct hva2 +{ + f32x4 a; + f32x4 b; +}; + +static struct hva2 +bump (struct hva2 s) +{ + s.a = s.a + 1; + s.b = s.b + 2; + return s; +} + +int +main (void) +{ + ffi_cif cif; + ffi_type vec_type; + ffi_type *vec_elems[5]; + ffi_type struct_type; + ffi_type *struct_elems[3]; + ffi_type *args[1]; + void *values[1]; + struct hva2 in = { { 1, 2, 3, 4 }, { 10, 20, 30, 40 } }; + struct hva2 out, ref; + int i; + + make_vector_type (&vec_type, vec_elems, &ffi_type_float, 4); + + struct_elems[0] = &vec_type; + struct_elems[1] = &vec_type; + struct_elems[2] = NULL; + struct_type.size = 0; + struct_type.alignment = 0; + struct_type.type = FFI_TYPE_STRUCT; + struct_type.elements = struct_elems; + + args[0] = &struct_type; + values[0] = ∈ + + CHECK (ffi_prep_cif (&cif, FFI_DEFAULT_ABI, 1, &struct_type, args) + == FFI_OK); + CHECK (struct_type.size == 32); + + ffi_call (&cif, FFI_FN (bump), &out, values); + + ref = bump (in); + for (i = 0; i < 4; i++) + { + printf ("a[%d] = %g (want %g), b[%d] = %g (want %g)\n", + i, (double) out.a[i], (double) ref.a[i], + i, (double) out.b[i], (double) ref.b[i]); + CHECK (out.a[i] == ref.a[i]); + CHECK (out.b[i] == ref.b[i]); + } + + exit (0); +} diff --git a/deps/libffi/testsuite/libffi.vector/vector_int32x4.c b/deps/libffi/testsuite/libffi.vector/vector_int32x4.c new file mode 100644 index 000000000000..eaa6b802bab5 --- /dev/null +++ b/deps/libffi/testsuite/libffi.vector/vector_int32x4.c @@ -0,0 +1,54 @@ +/* Area: ffi_call + Purpose: Pass and return a 16-byte int32x4 integer vector. Integer + lanes still travel in a vector register, unlike an HFA of ints. + Limitations: none. + PR: none. + Originator: libffi vector support. */ + +/* { dg-do run } */ + +#include "vector.h" + +typedef int i32x4 __attribute__((vector_size (16))); + +static i32x4 +add_i32x4 (i32x4 a, i32x4 b) +{ + return a + b; +} + +int +main (void) +{ + ffi_cif cif; + ffi_type vec_type; + ffi_type *vec_elems[5]; + ffi_type *args[2]; + void *values[2]; + i32x4 a = { 1, 2, 3, 4 }; + i32x4 b = { 5, 6, 7, 8 }; + i32x4 r, ref; + int i; + + make_vector_type (&vec_type, vec_elems, &ffi_type_sint32, 4); + + args[0] = &vec_type; + args[1] = &vec_type; + values[0] = &a; + values[1] = &b; + + CHECK (ffi_prep_cif (&cif, FFI_DEFAULT_ABI, 2, &vec_type, args) == FFI_OK); + CHECK (vec_type.size == 16); + CHECK (vec_type.alignment == 16); + + ffi_call (&cif, FFI_FN (add_i32x4), &r, values); + + ref = add_i32x4 (a, b); + for (i = 0; i < 4; i++) + { + printf ("r[%d] = %d (want %d)\n", i, r[i], ref[i]); + CHECK (r[i] == ref[i]); + } + + exit (0); +} diff --git a/deps/libffi/testsuite/libffi.vector/vector_validate.c b/deps/libffi/testsuite/libffi.vector/vector_validate.c new file mode 100644 index 000000000000..d923ab465fe4 --- /dev/null +++ b/deps/libffi/testsuite/libffi.vector/vector_validate.c @@ -0,0 +1,103 @@ +/* Area: ffi_prep_cif + Purpose: Validate that malformed vector type descriptors are rejected + with FFI_BAD_TYPEDEF, and that a well-formed vector is accepted + with the computed power-of-two size and min(size,16) alignment. + Limitations: none. + PR: none. + Originator: libffi vector support. */ + +/* { dg-do run } */ + +#include "vector.h" + +int +main (void) +{ + ffi_cif cif; + + /* Heterogeneous lanes (float mixed with double) -> FFI_BAD_TYPEDEF. */ + { + ffi_type vt; + ffi_type *elems[3]; + ffi_type *args[1]; + elems[0] = &ffi_type_float; + elems[1] = &ffi_type_double; + elems[2] = NULL; + vt.size = 0; + vt.alignment = 0; + vt.type = FFI_TYPE_VECTOR; + vt.elements = elems; + args[0] = &vt; + CHECK (ffi_prep_cif (&cif, FFI_DEFAULT_ABI, 1, &ffi_type_void, args) + == FFI_BAD_TYPEDEF); + } + + /* An empty (zero-lane) vector -> FFI_BAD_TYPEDEF. */ + { + ffi_type vt; + ffi_type *elems[1]; + ffi_type *args[1]; + elems[0] = NULL; + vt.size = 0; + vt.alignment = 0; + vt.type = FFI_TYPE_VECTOR; + vt.elements = elems; + args[0] = &vt; + CHECK (ffi_prep_cif (&cif, FFI_DEFAULT_ABI, 1, &ffi_type_void, args) + == FFI_BAD_TYPEDEF); + } + + /* A non-scalar (struct) lane type -> FFI_BAD_TYPEDEF. */ + { + ffi_type inner; + ffi_type *inner_elems[2]; + ffi_type vt; + ffi_type *elems[3]; + ffi_type *args[1]; + inner_elems[0] = &ffi_type_float; + inner_elems[1] = NULL; + inner.size = 0; + inner.alignment = 0; + inner.type = FFI_TYPE_STRUCT; + inner.elements = inner_elems; + elems[0] = &inner; + elems[1] = &inner; + elems[2] = NULL; + vt.size = 0; + vt.alignment = 0; + vt.type = FFI_TYPE_VECTOR; + vt.elements = elems; + args[0] = &vt; + CHECK (ffi_prep_cif (&cif, FFI_DEFAULT_ABI, 1, &ffi_type_void, args) + == FFI_BAD_TYPEDEF); + } + + /* A well-formed 3 x float vector is accepted with computed layout. */ + { + ffi_type vt; + ffi_type *elems[4]; + ffi_type *args[1]; + make_vector_type (&vt, elems, &ffi_type_float, 3); + args[0] = &vt; + CHECK (ffi_prep_cif (&cif, FFI_DEFAULT_ABI, 1, &ffi_type_void, args) + == FFI_OK); + CHECK (vt.size == 16); /* 12 rounded up to 16 */ + CHECK (vt.alignment == 16); /* min(16, 16) */ + } + + /* An 8-byte vector gets alignment 8 = min(8, 16). */ + { + ffi_type vt; + ffi_type *elems[3]; + ffi_type *args[1]; + make_vector_type (&vt, elems, &ffi_type_float, 2); + args[0] = &vt; + CHECK (ffi_prep_cif (&cif, FFI_DEFAULT_ABI, 1, &ffi_type_void, args) + == FFI_OK); + CHECK (vt.size == 8); + CHECK (vt.alignment == 8); + } + + printf ("vector validation ok\n"); + exit (0); +} diff --git a/deps/libffi/testsuite/libffi.vector/vector_vec3.c b/deps/libffi/testsuite/libffi.vector/vector_vec3.c new file mode 100644 index 000000000000..5a0367283023 --- /dev/null +++ b/deps/libffi/testsuite/libffi.vector/vector_vec3.c @@ -0,0 +1,73 @@ +/* Area: ffi_call + Purpose: Pass and return a three-lane float vector. Clang's + ext_vector_type(3) has 12 bytes of data padded to 16-byte + storage; libffi's power-of-two size rule must reproduce that + layout so a natively compiled callee agrees. + Limitations: Clang only (GCC's vector_size requires power-of-two totals and + rejects a 12-byte vector). A no-op on other compilers. + PR: none. + Originator: libffi vector support. */ + +/* { dg-do run } */ + +#include "vector.h" + +#ifdef __clang__ + +typedef float f3 __attribute__((ext_vector_type (3))); + +static f3 +scale3 (f3 v) +{ + f3 r; + r[0] = v[0] + 1; + r[1] = v[1] + 2; + r[2] = v[2] + 3; + return r; +} + +int +main (void) +{ + ffi_cif cif; + ffi_type vec_type; + ffi_type *vec_elems[4]; + ffi_type *args[1]; + void *values[1]; + f3 a = { 10, 20, 30 }; + f3 r, ref; + int i; + + make_vector_type (&vec_type, vec_elems, &ffi_type_float, 3); + + args[0] = &vec_type; + values[0] = &a; + + CHECK (ffi_prep_cif (&cif, FFI_DEFAULT_ABI, 1, &vec_type, args) == FFI_OK); + /* 3 x float = 12, rounded up to 16 (matches ext_vector_type storage). */ + CHECK (vec_type.size == 16); + CHECK (vec_type.alignment == 16); + CHECK (sizeof (f3) == 16); + + ffi_call (&cif, FFI_FN (scale3), &r, values); + + ref = scale3 (a); + for (i = 0; i < 3; i++) + { + printf ("r[%d] = %g (want %g)\n", i, (double) r[i], (double) ref[i]); + CHECK (r[i] == ref[i]); + } + + exit (0); +} + +#else + +int +main (void) +{ + /* ext_vector_type is a Clang extension; nothing to test elsewhere. */ + exit (0); +} + +#endif diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc index 1aadcf67bffe..d731e823b39d 100644 --- a/deps/ncrypto/ncrypto.cc +++ b/deps/ncrypto/ncrypto.cc @@ -539,8 +539,7 @@ bool setFipsEnabled(bool enable, CryptoErrorList* errors) { if (isFipsEnabled() == enable) return true; ClearErrorOnReturn clearErrorOnReturn(errors); #if OPENSSL_VERSION_MAJOR >= 3 - return EVP_default_properties_enable_fips(nullptr, enable ? 1 : 0) == 1 && - EVP_default_properties_is_fips_enabled(nullptr); + return EVP_default_properties_enable_fips(nullptr, enable ? 1 : 0) == 1; #else return FIPS_mode_set(enable ? 1 : 0) == 1; #endif @@ -5651,9 +5650,11 @@ DataPointer RSA_Cipher(const EVPKeyPointer& key, if (!key) return {}; EVPKeyCtxPointer ctx = key.newCtx(); + const Digest& mgf1_digest = + params.mgf1_digest != nullptr ? params.mgf1_digest : params.digest; if (!ctx || init(ctx.get()) <= 0 || !ctx.setRsaPadding(params.padding) || - (params.digest != nullptr && (!ctx.setRsaOaepMd(params.digest) || - !ctx.setRsaMgf1Md(params.digest)))) { + (params.digest != nullptr && + (!ctx.setRsaOaepMd(params.digest) || !ctx.setRsaMgf1Md(mgf1_digest)))) { return {}; } @@ -5692,7 +5693,9 @@ DataPointer CipherImpl(const EVPKeyPointer& key, if (!key) return {}; EVPKeyCtxPointer ctx = key.newCtx(); if (!ctx || init(ctx.get()) <= 0 || !ctx.setRsaPadding(params.padding) || - (params.digest != nullptr && !ctx.setRsaOaepMd(params.digest))) { + (params.digest != nullptr && !ctx.setRsaOaepMd(params.digest)) || + (params.mgf1_digest != nullptr && + !ctx.setRsaMgf1Md(params.mgf1_digest))) { return {}; } diff --git a/deps/ncrypto/ncrypto.h b/deps/ncrypto/ncrypto.h index 53302394f38b..a11d67ae460a 100644 --- a/deps/ncrypto/ncrypto.h +++ b/deps/ncrypto/ncrypto.h @@ -508,6 +508,7 @@ class Cipher final { struct CipherParams { int padding; Digest digest; + Digest mgf1_digest; const Buffer label; }; diff --git a/deps/openssl/openssl.gyp b/deps/openssl/openssl.gyp index 4e16412a0283..144085fd33df 100644 --- a/deps/openssl/openssl.gyp +++ b/deps/openssl/openssl.gyp @@ -36,7 +36,8 @@ # VC-WIN64-ARM inherits from VC-noCE-common that has no asms. 'includes': ['./openssl_no_asm.gypi'], }, 'gas_version and v(gas_version) >= v("2.26") or ' - 'nasm_version and v(nasm_version) >= v("2.11.8")', { + 'nasm_version and v(nasm_version) >= v("2.11.8") or ' + 'llvm_version and v(llvm_version) >= v("8.0")', { # Require AVX512IFMA supported. See # https://www.openssl.org/docs/man1.1.1/man3/OPENSSL_ia32cap.html # Currently crypto/poly1305/asm/poly1305-x86_64.pl requires AVX512IFMA. @@ -114,7 +115,8 @@ # VC-WIN64-ARM inherits from VC-noCE-common that has no asms. 'includes': ['./openssl-fips_no_asm.gypi'], }, 'gas_version and v(gas_version) >= v("2.26") or ' - 'nasm_version and v(nasm_version) >= v("2.11.8")', { + 'nasm_version and v(nasm_version) >= v("2.11.8") or ' + 'llvm_version and v(llvm_version) >= v("8.0")', { # Require AVX512IFMA supported. See # https://www.openssl.org/docs/man1.1.1/man3/OPENSSL_ia32cap.html # Currently crypto/poly1305/asm/poly1305-x86_64.pl requires AVX512IFMA. diff --git a/deps/perfetto/perfetto.gyp b/deps/perfetto/perfetto.gyp index 3836f3424cbd..083d0b386dd2 100644 --- a/deps/perfetto/perfetto.gyp +++ b/deps/perfetto/perfetto.gyp @@ -9,6 +9,7 @@ { 'target_name': 'perfetto_sdk', 'type': 'static_library', + 'toolsets': ['host', 'target'], 'include_dirs': [ 'sdk' ], 'direct_dependent_settings': { # Use like `#include "perfetto.h"` diff --git a/deps/undici/src/docs/docs/api/Client.md b/deps/undici/src/docs/docs/api/Client.md index dc6ab7a6d5f9..d48b2303f287 100644 --- a/deps/undici/src/docs/docs/api/Client.md +++ b/deps/undici/src/docs/docs/api/Client.md @@ -111,22 +111,35 @@ added: v1.0.0 `autoSelectFamily` is enabled. **Default:** `250`. * `allowH2` {boolean} Enables HTTP/2 support when the server assigns it a higher priority through ALPN negotiation. **Default:** `true`. - * `useH2c` {boolean} Enforces h2c (HTTP/2 cleartext) for non-HTTPS - connections. **Default:** `false`. - * `maxConcurrentStreams` {number} The maximum number of concurrent HTTP/2 + * `useH2c` {boolean} _Deprecated: use h2Options.useH2c instead_ Enforces h2c (HTTP/2 cleartext) for non-HTTPS + connections. **Default:** `false`. + * `maxConcurrentStreams` {number} _Deprecated: use h2Options.useH2c instead_ The maximum number of concurrent HTTP/2 streams for a single session. Once h2 is negotiated this — not `pipelining`, which is HTTP/1.1 only — is the ceiling used to dispatch in-flight requests. It may be overridden by the server's `SETTINGS_MAX_CONCURRENT_STREAMS` frame. **Default:** `100`. - * `initialWindowSize` {number} The HTTP/2 stream-level flow-control window - size (`SETTINGS_INITIAL_WINDOW_SIZE`). Must be a positive integer. - **Default:** `262144`. - * `connectionWindowSize` {number} The HTTP/2 connection-level flow-control + * `connectionWindowSize` {number} _Deprecated: use h2Options.connectionWindowSize instead_ The HTTP/2 connection-level flow-control window size set via `ClientHttp2Session.setLocalWindowSize()`. Must be a positive integer. **Default:** `524288`. - * `pingInterval` {number} The time interval, in milliseconds, between HTTP/2 + * `pingInterval` {number} _Deprecated: use h2Options.pingInterval instead_ The time interval, in milliseconds, between HTTP/2 PING frames. Set to `0` to disable PING frames. Applies only to HTTP/2 connections and emits a `ping` event on the client. **Default:** `60e3`. + * `h2Options` {object} Set of options for HTTP/2 sessions + * `useH2c` {boolean} Enforces h2c (HTTP/2 cleartext) for non-HTTPS + connections. **Default:** `false`. + * `maxConcurrentStreams` {number} The maximum number of concurrent HTTP/2 + streams for a single session. Once h2 is negotiated this — not `pipelining`, + which is HTTP/1.1 only — is the ceiling used to dispatch in-flight requests. + It may be overridden by the server's `SETTINGS_MAX_CONCURRENT_STREAMS` + frame. **Default:** `100`. + * `connectionWindowSize` {number} The HTTP/2 connection-level flow-control + window size set via `ClientHttp2Session.setLocalWindowSize()`. Must be a + positive integer. **Default:** `524288`. + * `pingInterval` {number} The time interval, in milliseconds, between HTTP/2 + PING frames. Set to `0` to disable PING frames. Applies only to HTTP/2 + connections and emits a `ping` event on the client. **Default:** `60e3`. + * `settings` {object} `SETTINGS` frame options. For full reference, take a + look to [HTTP/2#Settings Object](https://nodejs.org/api/http2.html#settings-object) * `webSocket` {Object} (optional) WebSocket-specific configuration. * `maxFragments` {number} The maximum number of fragments in a message. Set to `0` to disable the limit. **Default:** `131072`. diff --git a/deps/undici/src/lib/api/readable.js b/deps/undici/src/lib/api/readable.js index 71d90d457b34..e3dd3dcce4b7 100644 --- a/deps/undici/src/lib/api/readable.js +++ b/deps/undici/src/lib/api/readable.js @@ -15,7 +15,6 @@ const kContentType = Symbol('kContentType') const kContentLength = Symbol('kContentLength') const kUsed = Symbol('kUsed') const kBytesRead = Symbol('kBytesRead') -const kPreservedBuffer = Symbol('kPreservedBuffer') const noop = () => {} @@ -326,36 +325,14 @@ class BodyReadable extends Readable { */ setEncoding (encoding) { if (Buffer.isEncoding(encoding)) { - // Preserve raw Buffer chunks for the consume path (body.text(), - // body.json(), etc.) before super.setEncoding() replaces them - // with decoded strings. Without this, the consume path would - // lose access to the original bytes — some of which may be held - // by the decoder for incomplete multi-byte sequences, and the - // rest converted to strings that can't be safely concatenated - // byte-wise. - const state = this._readableState - const buffer = state.buffer - if (buffer && state.length > 0) { - const bufferIndex = state.bufferIndex ?? 0 - const preserved = [] - const source = typeof buffer.slice === 'function' - ? buffer.slice(bufferIndex) - : buffer - for (const data of source) { - if (Buffer.isBuffer(data)) { - preserved.push(data) - } - } - if (preserved.length > 0) { - this[kPreservedBuffer] = (this[kPreservedBuffer] || []).concat(preserved) - } - } - // Delegate to Node.js Readable.setEncoding() which initializes a // StringDecoder and re-encodes already-buffered chunks. This properly // handles multi-byte sequences split at chunk boundaries for the // for-await / on('data') paths. Without this, Node.js uses // buf.toString(encoding) on each chunk, producing U+FFFD for split chars. + // + // The consume path (body.text(), body.json(), ...) copes with the + // decoded strings this leaves in state.buffer, see consumeStart(). super.setEncoding(encoding) } return this @@ -464,17 +441,7 @@ function consumeStart (consume) { const { _readableState: state } = consume.stream - // If setEncoding() was called, state.buffer may contain decoded strings - // (which would break Buffer.concat in chunksDecode). Use the preserved - // raw Buffers (saved before super.setEncoding() in setEncoding()) for - // byte-level accurate consumption. Otherwise read from state.buffer. - const preserved = consume.stream[kPreservedBuffer] - if (preserved && preserved.length > 0) { - for (const chunk of preserved) { - consumePush(consume, chunk) - } - consume.stream[kPreservedBuffer] = null - } else if (state.bufferIndex) { + if (state.bufferIndex) { const start = state.bufferIndex const end = state.buffer.length for (let n = start; n < end; n++) { @@ -486,14 +453,29 @@ function consumeStart (consume) { } } + // If setEncoding() was called, state.buffer holds decoded strings, which + // consumePush() turns back into bytes. The trailing bytes of a multi-byte + // sequence split across a chunk boundary are not part of any of those + // strings, they are held inside the decoder until the rest arrives, so + // take them from there. + const decoder = state.decoder + if (decoder != null && decoder.lastNeed > 0) { + consumePush(consume, Buffer.from(decoder.lastChar.subarray(0, decoder.lastTotal - decoder.lastNeed))) + } + if (state.endEmitted) { - consumeEnd(this[kConsume], this._readableState.encoding) - } else { - consume.stream.on('end', function () { - consumeEnd(this[kConsume], this._readableState.encoding) - }) + // No `this` to read the consume off here: consumeStart is a free function, called from + // the queueMicrotask above. The callback below does have one, because the emitter passes + // the stream as its receiver. Returning matters too - consumeEnd() clears consume.stream, + // which the resume() below would then dereference. + consumeEnd(consume, state.encoding) + return } + consume.stream.on('end', function () { + consumeEnd(this[kConsume], this._readableState.encoding) + }) + consume.stream.resume() while (consume.stream.read() != null) { @@ -583,7 +565,7 @@ function consumeEnd (consume, encoding) { /** * @param {Consume} consume - * @param {Buffer} chunk + * @param {Buffer|string} chunk * @returns {void} */ function consumePush (consume, chunk) { @@ -591,6 +573,14 @@ function consumePush (consume, chunk) { return } + if (typeof chunk === 'string') { + // Buffered before the consume started, while an encoding was set. + // consume.length has to stay a byte count and chunksDecode()/chunksConcat() + // only work on bytes, so re-encode. A string's own length is in UTF-16 code + // units and Uint8Array.prototype.set() ignores a string argument entirely. + chunk = Buffer.from(chunk, consume.stream._readableState.encoding) + } + consume.length += chunk.length consume.body.push(chunk) } diff --git a/deps/undici/src/lib/core/connect.js b/deps/undici/src/lib/core/connect.js index ad962c31944a..f729dfb0526d 100644 --- a/deps/undici/src/lib/core/connect.js +++ b/deps/undici/src/lib/core/connect.js @@ -105,13 +105,27 @@ function buildConnector ({ allowH2, preferH2, useH2c, maxCachedSessions, socketP port = port || 80 - socket = net.connect({ + const connectOptions = { highWaterMark: 64 * 1024, // Same as nodejs fs streams. ...options, localAddress, port, host: hostname - }) + } + + const family = net.isIP(hostname) + if (family !== 0 && servername && servername !== hostname) { + connectOptions.host = servername + connectOptions.lookup = (_hostname, lookupOptions, cb) => { + if (lookupOptions.all) { + cb(null, [{ address: hostname, family }]) + } else { + cb(null, hostname, family) + } + } + } + + socket = net.connect(connectOptions) if (useH2c === true) { socket.alpnProtocol = 'h2' } diff --git a/deps/undici/src/lib/core/symbols.js b/deps/undici/src/lib/core/symbols.js index 8bad25eed9fd..badecb709086 100644 --- a/deps/undici/src/lib/core/symbols.js +++ b/deps/undici/src/lib/core/symbols.js @@ -56,6 +56,7 @@ module.exports = { kCounter: Symbol('socket request counter'), kMaxResponseSize: Symbol('max response size'), kHTTP2Session: Symbol('http2Session'), + kHTTP2Options: Symbol('http2 options'), kHTTP2SessionState: Symbol('http2Session state'), kRetryHandlerDefaultRetry: Symbol('retry agent default retry'), kConstruct: Symbol('constructable'), diff --git a/deps/undici/src/lib/dispatcher/client-h1.js b/deps/undici/src/lib/dispatcher/client-h1.js index abf381f65a46..9f6f17c1579b 100644 --- a/deps/undici/src/lib/dispatcher/client-h1.js +++ b/deps/undici/src/lib/dispatcher/client-h1.js @@ -1052,7 +1052,7 @@ function onSocketClose () { function clearIdleSocketValidation (socket) { if (socket[kIdleSocketValidationTimeout]) { - clearImmediate(socket[kIdleSocketValidationTimeout]) + clearTimeout(socket[kIdleSocketValidationTimeout]) socket[kIdleSocketValidationTimeout] = null } @@ -1061,14 +1061,14 @@ function clearIdleSocketValidation (socket) { function scheduleIdleSocketValidation (client, socket) { socket[kIdleSocketValidation] = 1 - socket[kIdleSocketValidationTimeout] = setImmediate(() => { + socket[kIdleSocketValidationTimeout] = setTimeout(() => { socket[kIdleSocketValidationTimeout] = null socket[kIdleSocketValidation] = 2 if (client[kSocket] === socket && !socket.destroyed) { client[kResume]() } - }) + }, 0) socket[kIdleSocketValidationTimeout].unref?.() } diff --git a/deps/undici/src/lib/dispatcher/client-h2.js b/deps/undici/src/lib/dispatcher/client-h2.js index 19622db68ace..bc401008f004 100644 --- a/deps/undici/src/lib/dispatcher/client-h2.js +++ b/deps/undici/src/lib/dispatcher/client-h2.js @@ -26,10 +26,7 @@ const { kStrictContentLength, kOnError, kMaxConcurrentStreams, - kPingInterval, kHTTP2Session, - kHTTP2InitialWindowSize, - kHTTP2ConnectionWindowSize, kHostAuthority, kResume, kSize, @@ -41,7 +38,8 @@ const { kEnableConnectProtocol, kRemoteSettings, kHTTP2Stream, - kHTTP2SessionState + kHTTP2SessionState, + kHTTP2Options } = require('../core/symbols.js') const { channels } = require('../core/diagnostics.js') @@ -51,6 +49,14 @@ const kRequestStream = Symbol('request stream') const kRequestStreamCleanup = Symbol('request stream cleanup') const kRequestStreamState = Symbol('request stream state') const kReceivedGoAway = Symbol('received goaway') +const kGoAwayReplayAttempts = Symbol('goaway replay attempts') +const kRefusedStreamRetry = Symbol('refused stream retry') + +// RFC 9113 section 8.7: a client SHOULD NOT automatically retry a request more +// than once. Without a budget a peer that keeps refusing turns one request into +// an unbounded connect/refuse/reconnect loop that never settles and starves the +// event loop. +const MAX_GOAWAY_REPLAY_ATTEMPTS = 1 let extractBody @@ -179,12 +185,24 @@ function completeRequest (client, request, resetPendingIdx = false) { } } -function canRetryRequestAfterGoAway (request) { +function canReplayRequest (request) { const { body } = request return body == null || util.isBuffer(body) || util.isBlobLike(body) } +// Count a GOAWAY refusal against the request's replay budget. A peer that +// refuses every connection must eventually surface an error to the caller +// rather than being retried forever. Kept separate from canReplayRequest so +// that the REFUSED_STREAM retry, which has its own single-attempt limit, does +// not consume this budget just by asking whether the body can be replayed. +function registerGoAwayRefusal (request) { + const attempts = (request[kGoAwayReplayAttempts] ?? 0) + 1 + request[kGoAwayReplayAttempts] = attempts + + return attempts <= MAX_GOAWAY_REPLAY_ATTEMPTS +} + function closeStream (stream, code = NGHTTP2_REFUSED_STREAM) { if (stream != null && !stream.destroyed && !stream.closed) { try { @@ -197,19 +215,44 @@ function detachRequestStreamForClose (request) { const stream = request[kRequestStream] clearRequestStream(request) + severRequestStream(stream) return stream } +// Unbind a stream from its request for good. releaseRequestStream() alone +// leaves the 'close' listener attached and kRequestStreamState populated, so a +// stream abandoned here would still run completeRequestStream() later — and +// splice out the request that has since been requeued onto another session. +function severRequestStream (stream) { + if (stream == null || stream[kRequestStreamState] == null) { + return + } + + stream[kRequestStreamState] = null + stream.off('close', completeRequestStream) + // Upgrade streams use their own close cleanup, which would otherwise release + // the session a second time after the stream has been severed for GOAWAY. + stream.off('close', onUpgradeStreamClose) + + if (stream[kHTTP2Session] != null) { + closeStreamSession(stream) + } + + if (!stream.destroyed && !stream.closed) { + stream.once('error', noop) + } +} + function connectH2 (client, socket) { client[kSocket] = socket - const http2InitialWindowSize = client[kHTTP2InitialWindowSize] - const http2ConnectionWindowSize = client[kHTTP2ConnectionWindowSize] + const http2InitialWindowSize = client[kHTTP2Options].sessionOptions?.initialWindowSize + const http2ConnectionWindowSize = client[kHTTP2Options].connectionWindowSize const session = http2.connect(client[kUrl], { createConnection: () => socket, - peerMaxConcurrentStreams: client[kMaxConcurrentStreams], + peerMaxConcurrentStreams: client[kHTTP2Options].maxConcurrentStreams, settings: { // TODO(metcoder95): add support for PUSH enablePush: false, @@ -223,13 +266,16 @@ function connectH2 (client, socket) { session[kSocket] = socket session[kHTTP2SessionState] = { idleTimeout: null, + // Armed while the peer advertises MAX_CONCURRENT_STREAMS = 0 and we have + // work that cannot start. See setNoStreamsTimeout. + noStreamsTimeout: null, // Sockets start out ref'd. Session ref/unref proxies to the socket, so a // single cached flag lets us skip redundant uv ref/unref calls, provided // every ref/unref of the session or its socket goes through // refH2Session/unrefH2Session. refed: true, ping: { - interval: client[kPingInterval] === 0 ? null : setInterval(onHttp2SendPing, client[kPingInterval], session).unref() + interval: client[kHTTP2Options].pingInterval === 0 ? null : setInterval(onHttp2SendPing, client[kHTTP2Options].pingInterval, session).unref() } } session[kReceivedGoAway] = false @@ -369,7 +415,74 @@ function resumeH2 (client) { } else { clearHttp2IdleTimeout(session) } + + if (client[kMaxConcurrentStreams] === 0 && client[kRunning] === 0 && client[kPending] > 0) { + setNoStreamsTimeout(session) + } else { + clearNoStreamsTimeout(session) + } + } +} + +function clearNoStreamsTimeout (session) { + const state = session[kHTTP2SessionState] + + if (state?.noStreamsTimeout != null) { + clearTimeout(state.noStreamsTimeout) + state.noStreamsTimeout = null + } +} + +// A peer is allowed to advertise SETTINGS_MAX_CONCURRENT_STREAMS = 0 to refuse +// new streams (RFC 9113 §6.5.2), and is expected to raise it again later. Until +// it does, busy() reports the client as permanently busy and queued requests +// cannot open a stream — which means no per-stream timeout covers them, and no +// reconnect can happen either, so the SETTINGS frame that would lift the limit +// can never arrive. Give the peer headersTimeout to start honouring requests +// before failing them; a request that cannot even be sent has missed the same +// deadline as one whose headers never arrive. +function setNoStreamsTimeout (session) { + const client = session[kClient] + const state = session[kHTTP2SessionState] + const timeout = client[kHeadersTimeout] + + if (!timeout || state.noStreamsTimeout != null) { + return + } + + state.noStreamsTimeout = setTimeout(onNoStreamsTimeout, timeout, session).unref() +} + +function onNoStreamsTimeout (session) { + const client = session[kClient] + const state = session[kHTTP2SessionState] + + state.noStreamsTimeout = null + + if ( + client[kHTTP2Session] !== session || + client[kMaxConcurrentStreams] !== 0 || + client[kRunning] !== 0 || + client[kPending] === 0 + ) { + return + } + + const err = new HeadersTimeoutError( + `HTTP/2: server did not accept a new stream within ${client[kHeadersTimeout]}` + ) + + const requests = client[kQueue].splice(client[kPendingIdx]) + for (let i = 0; i < requests.length; i++) { + if (requests[i] != null) { + util.errorRequest(client, requests[i], err) + } } + + // Drop the unusable session so the next request gets a fresh connection, + // whose SETTINGS may well allow streams again. + session[kError] = err + resetHttp2Session(session, err) } function clearHttp2IdleTimeout (session) { @@ -527,7 +640,7 @@ function onHttp2SessionGoAway (errorCode, lastStreamID) { if (request != null) { streamsToClose.push(detachRequestStreamForClose(request)) - if (canRetryRequestAfterGoAway(request)) { + if (canReplayRequest(request) && registerGoAwayRefusal(request)) { retriableRequests.push(request) } else { util.errorRequest(client, request, err) @@ -552,6 +665,7 @@ function onHttp2SessionGoAway (errorCode, lastStreamID) { } clearHttp2IdleTimeout(this) + clearNoStreamsTimeout(this) if (!this.closed && !this.destroyed) { this.close() @@ -576,6 +690,7 @@ function onHttp2SessionClose () { } clearHttp2IdleTimeout(this) + clearNoStreamsTimeout(this) if (state.ping.interval != null) { clearInterval(state.ping.interval) @@ -687,6 +802,16 @@ function completeRequestStream () { if (state.pendingEnd && !state.request.aborted && !state.request.completed) { state.request.onResponseEnd(state.trailers || {}) + } else if (!state.request.aborted && !state.request.completed) { + // The stream closed without a complete response and without reporting an + // error. finalizeRequest() below frees the queue slot either way, so + // without this the request would simply vanish and its caller would never + // hear back. + util.errorRequest( + state.client, + state.request, + new InformationalError('HTTP/2: stream closed before the response was complete') + ) } finalizeRequest(state) @@ -1286,6 +1411,38 @@ function onEnd () { } } +function retryRefusedStream (stream, state) { + const { client, request } = state + + if ( + state.responseReceived || + request.aborted || + request.completed || + request[kRefusedStreamRetry] || + !canReplayRequest(request) + ) { + return false + } + + // RFC 9113 section 8.7 permits retrying REFUSED_STREAM, but says clients + // SHOULD NOT automatically retry the same request more than once. + request[kRefusedStreamRetry] = true + + // Detach the failed attempt before moving the request back to the pending + // queue. The peer only reset this stream, so the HTTP/2 session remains + // usable for the retry. Severing also drops the 'close' listener, so the + // abandoned stream cannot later complete the retried request. + detachRequestStreamForClose(request) + state.stream = null + state.requestFinalized = true + + completeRequest(client, request) + client[kQueue].splice(client[kPendingIdx], 0, request) + client[kResume]() + + return true +} + function onError (err) { const stream = this const state = stream[kRequestStreamState] @@ -1295,6 +1452,18 @@ function onError (err) { } stream.off('error', onError) + + if (typeof stream.rstCode === 'number' && stream.rstCode !== NGHTTP2_NO_ERROR) { + err.http2ErrorCode = stream.rstCode + } + + if ( + stream.rstCode === NGHTTP2_REFUSED_STREAM && + retryRefusedStream(stream, state) + ) { + return + } + state.abort(err) } diff --git a/deps/undici/src/lib/dispatcher/client.js b/deps/undici/src/lib/dispatcher/client.js index 8a4f65171bd9..d620e8310cfb 100644 --- a/deps/undici/src/lib/dispatcher/client.js +++ b/deps/undici/src/lib/dispatcher/client.js @@ -53,10 +53,8 @@ const { kHTTPContext, kMaxConcurrentStreams, kHostAuthority, - kHTTP2InitialWindowSize, - kHTTP2ConnectionWindowSize, kResume, - kPingInterval + kHTTP2Options } = require('../core/symbols.js') const connectH1 = require('./client-h1.js') const connectH2 = require('./client-h2.js') @@ -76,6 +74,16 @@ function getPipelining (client) { return client[kPipelining] ?? client[kHTTPContext]?.defaultPipelining ?? 1 } +let h2NamespaceOptsWarning = false +function emitH2OptionsNamespaceWarning (optName) { + if (h2NamespaceOptsWarning === true) return + + process.emitWarning(`Use h2Options.${optName} instead. ${optName} for H2 will be deprecated in future major.`, { + code: 'UNDICI-H2-OPTIONS' + }) + h2NamespaceOptsWarning = true +} + // Protocol-aware dispatch ceiling. h1 RFC7230 pipelining is unrelated to h2 // stream multiplexing — over h2 the ceiling is the (server-confirmed) // maxConcurrentStreams. Before a context is attached we use the h1 @@ -128,7 +136,8 @@ class Client extends DispatcherBase { initialWindowSize, connectionWindowSize, pingInterval, - webSocket + webSocket, + h2Options } = {}) { if (keepAlive !== undefined) { throw new InvalidArgumentError('unsupported keepAlive, use pipelining=0 instead') @@ -216,24 +225,55 @@ class Client extends DispatcherBase { throw new InvalidArgumentError('allowH2 must be a valid boolean value') } - if (maxConcurrentStreams != null && (typeof maxConcurrentStreams !== 'number' || maxConcurrentStreams < 1)) { - throw new InvalidArgumentError('maxConcurrentStreams must be a positive integer, greater than 0') - } + // We validate only if allowH2 is enabled or null (enabled by default) + if (allowH2 !== false) { + // Prioritise new h2Options object, otherwise fallback to prior configuration options + if (h2Options != null) { + if (h2Options.useH2c != null && typeof h2Options.useH2c !== 'boolean') { + throw new InvalidArgumentError('h2Options.useH2c must be a valid boolean value') + } - if (useH2c != null && typeof useH2c !== 'boolean') { - throw new InvalidArgumentError('useH2c must be a valid boolean value') - } + if (h2Options.settings?.initialWindowSize != null && (!Number.isInteger(h2Options.settings.initialWindowSize) || h2Options.settings.initialWindowSize < 1)) { + throw new InvalidArgumentError('h2Options.settings.initialWindowSize must be a positive integer, greater than 0') + } - if (initialWindowSize != null && (!Number.isInteger(initialWindowSize) || initialWindowSize < 1)) { - throw new InvalidArgumentError('initialWindowSize must be a positive integer, greater than 0') - } + if (h2Options.maxConcurrentStreams != null && (!Number.isInteger(h2Options.connectionWindowSize) || h2Options.maxConcurrentStreams < 1)) { + throw new InvalidArgumentError('h2Options.maxConcurrentStreams must be a positive integer, greater than 0') + } - if (connectionWindowSize != null && (!Number.isInteger(connectionWindowSize) || connectionWindowSize < 1)) { - throw new InvalidArgumentError('connectionWindowSize must be a positive integer, greater than 0') - } + if (h2Options.connectionWindowSize != null && (!Number.isInteger(h2Options.connectionWindowSize) || h2Options.connectionWindowSize < 1)) { + throw new InvalidArgumentError('h2Options.connectionWindowSize must be a positive integer, greater than 0') + } + + if (h2Options.pingInterval != null && (typeof h2Options.pingInterval !== 'number' || !Number.isInteger(h2Options.pingInterval) || h2Options.pingInterval < 0)) { + throw new InvalidArgumentError('h2Options.pingInterval must be a positive integer, greater or equal to 0') + } + } else { + if (useH2c != null && typeof useH2c !== 'boolean') { + emitH2OptionsNamespaceWarning('useH2c') + throw new InvalidArgumentError('useH2c must be a valid boolean value') + } + + if (maxConcurrentStreams != null && (typeof maxConcurrentStreams !== 'number' || maxConcurrentStreams < 1)) { + emitH2OptionsNamespaceWarning('maxConcurrentStreams') + throw new InvalidArgumentError('maxConcurrentStreams must be a positive integer, greater than 0') + } - if (pingInterval != null && (typeof pingInterval !== 'number' || !Number.isInteger(pingInterval) || pingInterval < 0)) { - throw new InvalidArgumentError('pingInterval must be a positive integer, greater or equal to 0') + if (initialWindowSize != null && (!Number.isInteger(initialWindowSize) || initialWindowSize < 1)) { + emitH2OptionsNamespaceWarning('initialWindowSize') + throw new InvalidArgumentError('initialWindowSize must be a positive integer, greater than 0') + } + + if (connectionWindowSize != null && (!Number.isInteger(connectionWindowSize) || connectionWindowSize < 1)) { + emitH2OptionsNamespaceWarning('connectionWindowSize') + throw new InvalidArgumentError('connectionWindowSize must be a positive integer, greater than 0') + } + + if (pingInterval != null && (typeof pingInterval !== 'number' || !Number.isInteger(pingInterval) || pingInterval < 0)) { + emitH2OptionsNamespaceWarning('pingInterval') + throw new InvalidArgumentError('pingInterval must be a positive integer, greater or equal to 0') + } + } } super({ webSocket }) @@ -243,8 +283,8 @@ class Client extends DispatcherBase { ...tls, maxCachedSessions, allowH2, - useH2c, socketPath, + useH2c: h2Options?.useH2c ?? useH2c, timeout: connectTimeout, ...(typeof autoSelectFamily === 'boolean' ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined), ...connect @@ -280,16 +320,20 @@ class Client extends DispatcherBase { this[kMaxResponseSize] = maxResponseSize > -1 ? maxResponseSize : -1 this[kHTTPContext] = null // h2 - this[kMaxConcurrentStreams] = maxConcurrentStreams != null ? maxConcurrentStreams : 100 // Max peerConcurrentStreams for a Node h2 server - // HTTP/2 window sizes are set to higher defaults than Node.js core for better performance: - // - initialWindowSize: 262144 (256KB) vs Node.js default 65535 (64KB - 1) - // Allows more data to be sent before requiring acknowledgment, improving throughput - // especially on high-latency networks. This matches common production HTTP/2 servers. - // - connectionWindowSize: 524288 (512KB) vs Node.js default (none set) - // Provides better flow control for the entire connection across multiple streams. - this[kHTTP2InitialWindowSize] = initialWindowSize != null ? initialWindowSize : 262144 - this[kHTTP2ConnectionWindowSize] = connectionWindowSize != null ? connectionWindowSize : 524288 - this[kPingInterval] = pingInterval != null ? pingInterval : 60e3 // Default ping interval for h2 - 1 minute + this[kHTTP2Options] = { + pingInterval: h2Options?.pingInterval ?? pingInterval ?? 60e3, + connectionWindowSize: h2Options?.connectionWindowSize ?? connectionWindowSize ?? 524288, + maxConcurrentStreams: h2Options?.maxConcurrentStreams ?? maxConcurrentStreams ?? 100, // Max peerConcurrentStreams for a Node h2 server + sessionOptions: { + // HTTP/2 window sizes are set to higher defaults than Node.js core for better performance: + // - initialWindowSize: 262144 (256KB) vs Node.js default 65535 (64KB - 1) + // Allows more data to be sent before requiring acknowledgment, improving throughput + // especially on high-latency networks. This matches common production HTTP/2 servers. + // - connectionWindowSize: 524288 (512KB) vs Node.js default (none set) + // Provides better flow control for the entire connection across multiple streams. + initialWindowSize: h2Options?.initialWindowSize ?? initialWindowSize ?? 262144 + } + } // kQueue is built up of 3 sections separated by // the kRunningIdx and kPendingIdx indices. @@ -672,6 +716,7 @@ function _resume (client, sync) { } if (!client[kHTTPContext]) { + client[kServerName] = request.servername connect(client) return } diff --git a/deps/undici/src/lib/dispatcher/env-http-proxy-agent.js b/deps/undici/src/lib/dispatcher/env-http-proxy-agent.js index f88437f1936a..51c50601714b 100644 --- a/deps/undici/src/lib/dispatcher/env-http-proxy-agent.js +++ b/deps/undici/src/lib/dispatcher/env-http-proxy-agent.js @@ -65,9 +65,10 @@ class EnvHttpProxyAgent extends DispatcherBase { #getProxyAgentForUrl (url) { let { protocol, host: hostname, port } = url - // Stripping ports in this way instead of using parsedUrl.hostname to make - // sure that the brackets around IPv6 addresses are kept. - hostname = hostname.replace(/:\d*$/, '').toLowerCase() + // Remove the port suffix (e.g. ":8080") and then strip surrounding + // brackets from IPv6 literals (e.g. "[::1]" -> "::1") so that the + // result matches the unbracketed form stored by #parseNoProxy. + hostname = hostname.replace(/:\d*$/, '').replace(/^\[(.+)\]$/, '$1').toLowerCase() port = Number.parseInt(port, 10) || DEFAULT_PORTS[protocol] || 0 if (!this.#shouldProxy(hostname, port)) { return this[kNoProxyAgent] @@ -119,11 +120,32 @@ class EnvHttpProxyAgent extends DispatcherBase { if (!entry) { continue } - const parsed = entry.match(/^(.+):(\d+)$/) + + // An IPv6 entry with a port must be bracketed: [::1]:443. + // A bare IPv6 address like ::1 contains colons that must not be + // confused with a host:port separator, so we handle it separately. + let hostname, port + const ipv6WithPort = entry.match(/^\[(.+)\]:(\d+)$/) + if (ipv6WithPort) { + hostname = ipv6WithPort[1] + port = Number.parseInt(ipv6WithPort[2], 10) + } else { + // Bracketed IPv6 without port, or plain hostname[:port], or bare IPv6. + // Strip optional brackets first. + const unbracketed = entry.replace(/^\[(.+)\]$/, '$1') + // A bare IPv6 address contains multiple colons; a hostname:port entry + // has exactly one colon followed by digits. Only attempt host:port + // splitting when that is unambiguously the case. + const colonCount = (unbracketed.match(/:/g) || []).length + const parsed = colonCount === 1 && unbracketed.match(/^(.+):(\d+)$/) + hostname = parsed ? parsed[1] : unbracketed + port = parsed ? Number.parseInt(parsed[2], 10) : 0 + } + noProxyEntries.push({ // strip leading dot or asterisk with dot - hostname: (parsed ? parsed[1] : entry).replace(/^\*?\./, '').toLowerCase(), - port: parsed ? Number.parseInt(parsed[2], 10) : 0 + hostname: hostname.replace(/^\*?\./, '').toLowerCase(), + port }) } diff --git a/deps/undici/src/lib/dispatcher/socks5-proxy-agent.js b/deps/undici/src/lib/dispatcher/socks5-proxy-agent.js index bb46b7cfa184..909c7f502478 100644 --- a/deps/undici/src/lib/dispatcher/socks5-proxy-agent.js +++ b/deps/undici/src/lib/dispatcher/socks5-proxy-agent.js @@ -6,7 +6,7 @@ let tls // include tls conditionally since it is not always available const DispatcherBase = require('./dispatcher-base') const { InvalidArgumentError } = require('../core/errors') const { Socks5Client, STATES } = require('../core/socks5-client') -const { kDispatch, kClose, kDestroy } = require('../core/symbols') +const { kBusy, kConnected, kDispatch, kClose, kDestroy } = require('../core/symbols') const Pool = require('./pool') const buildConnector = require('../core/connect') const { debuglog } = require('node:util') @@ -226,6 +226,20 @@ class Socks5ProxyAgent extends DispatcherBase { } }) this[kPools].set(originKey, pool) + + const closePoolIfUnused = () => { + if (this[kPools].get(originKey) !== pool || pool[kConnected] > 0 || pool[kBusy]) { + return + } + + this[kPools].delete(originKey) + if (!pool.destroyed) { + pool.close() + } + } + + pool.on('disconnect', closePoolIfUnused) + pool.on('connectionError', closePoolIfUnused) } // Dispatch the request through the per-origin pool diff --git a/deps/undici/src/lib/handler/retry-handler.js b/deps/undici/src/lib/handler/retry-handler.js index 3fc26229a1cc..c098b510c26c 100644 --- a/deps/undici/src/lib/handler/retry-handler.js +++ b/deps/undici/src/lib/handler/retry-handler.js @@ -241,6 +241,11 @@ class RetryHandler { } onResponseStart (controller, statusCode, headers, statusMessage) { + if (statusCode < 200) { + this.handler.onResponseStart?.(this.controllerProxy, statusCode, headers, statusMessage) + return + } + this.error = null this.retryCount += 1 this.statusCode = statusCode @@ -305,7 +310,7 @@ class RetryHandler { // First time we receive 206 const range = parseRangeHeader(headers['content-range']) - if (range == null) { + if (range == null || range.end == null) { this.headersSent = true this.handler.onResponseStart?.( this.controllerProxy, @@ -330,7 +335,7 @@ class RetryHandler { } // We make our best to checkpoint the body for further range headers - if (this.end == null) { + if (this.end == null && this.opts.method !== 'HEAD') { const contentLength = headers['content-length'] this.end = contentLength != null ? Number(contentLength) - 1 : null } diff --git a/deps/undici/src/lib/interceptor/cache.js b/deps/undici/src/lib/interceptor/cache.js index f50c1b7b67dc..2d7d01f130aa 100644 --- a/deps/undici/src/lib/interceptor/cache.js +++ b/deps/undici/src/lib/interceptor/cache.js @@ -540,13 +540,16 @@ module.exports = (opts = {}) => { return dispatch => { return (opts, handler) => { - if (!opts.origin || arrayIncludes(safeMethodsToNotCache, opts.method)) { - // Not a method we want to cache or we don't have the origin, skip + if (arrayIncludes(safeMethodsToNotCache, opts.method)) { + // Not a method we want to cache, skip return dispatch(opts, handler) } // Check if origin is in whitelist if (origins !== undefined) { + if (!opts.origin) { + return dispatch(opts, handler) + } const requestOrigin = opts.origin.toString().toLowerCase() let isAllowed = false diff --git a/deps/undici/src/lib/interceptor/deduplicate.js b/deps/undici/src/lib/interceptor/deduplicate.js index e81525ac5ea7..bacfeb3fb37e 100644 --- a/deps/undici/src/lib/interceptor/deduplicate.js +++ b/deps/undici/src/lib/interceptor/deduplicate.js @@ -59,7 +59,7 @@ module.exports = (opts = {}) => { return dispatch => { return (opts, handler) => { - if (!opts.origin || methods.includes(opts.method) === false) { + if (opts.upgrade || methods.includes(opts.method) === false) { return dispatch(opts, handler) } diff --git a/deps/undici/src/lib/llhttp/wasm_build_env.txt b/deps/undici/src/lib/llhttp/wasm_build_env.txt index e4cfa0c37626..82569ca62dd1 100644 --- a/deps/undici/src/lib/llhttp/wasm_build_env.txt +++ b/deps/undici/src/lib/llhttp/wasm_build_env.txt @@ -1,5 +1,5 @@ -> undici@8.9.0 build:wasm +> undici@8.10.0 build:wasm > node build/wasm.js --docker > docker run --rm --platform=linux/x86_64 --user 1001:1001 --mount type=bind,source=/home/runner/work/node/node/deps/undici/src/lib/llhttp,target=/home/node/build/lib/llhttp --mount type=bind,source=/home/runner/work/node/node/deps/undici/src/build,target=/home/node/build/build --mount type=bind,source=/home/runner/work/node/node/deps/undici/src/deps,target=/home/node/build/deps -t ghcr.io/nodejs/wasm-builder@sha256:975f391d907e42a75b8c72eb77c782181e941608687d4d8694c3e9df415a0970 node build/wasm.js diff --git a/deps/undici/src/lib/mock/mock-utils.js b/deps/undici/src/lib/mock/mock-utils.js index 111a860e9aee..e43f7218d0b4 100644 --- a/deps/undici/src/lib/mock/mock-utils.js +++ b/deps/undici/src/lib/mock/mock-utils.js @@ -17,6 +17,7 @@ const { } } = require('node:util') const { InvalidArgumentError } = require('../core/errors') +const requestAborted = Symbol('request aborted') function matchValue (match, value) { if (typeof match === 'string') { @@ -153,6 +154,11 @@ function getResponseData (data) { return data } else if (data instanceof ArrayBuffer) { return data + } else if (ArrayBuffer.isView(data)) { + // A DataView, or any non-Uint8Array typed array, is a byte container + // rather than a plain object. Buffer.from() cannot read one directly, so + // expose the bytes it covers instead of letting it reach JSON.stringify. + return new Uint8Array(data.buffer, data.byteOffset, data.byteLength) } else if (typeof data === 'object') { return JSON.stringify(data) } else if (data) { @@ -225,9 +231,15 @@ function deleteMockDispatch (mockDispatches, key) { } /** - * @param {string} path Path to remove trailing slash from + * @param {string|RegExp|Function} path Path, or path matcher, to remove trailing slash from */ function removeTrailingSlash (path) { + // Registered path matchers may be a RegExp or a function, which have no + // trailing slash to strip; hand those back for matchValue to apply. + if (typeof path !== 'string') { + return path + } + while (path.endsWith('/')) { path = path.slice(0, -1) } @@ -302,9 +314,13 @@ function mockDispatch (opts, handler) { mockDispatch.consumed = !mockDispatch.persist && timesInvoked >= times mockDispatch.pending = timesInvoked < times + const hasBodyHooks = typeof handler.onBodySent === 'function' || + typeof handler.onRequestSent === 'function' + // Here's where we resolve a callback if a callback is present for the dispatch data. - if (mockDispatch.data.callback) { - const callbackResult = mockDispatch.data.callback(opts) + if (mockDispatch.data.callback && (!hasBodyHooks || opts.body == null)) { + const { callback, ...responseDefaults } = mockDispatch.data + const callbackResult = callback(opts) // An asynchronous reply options callback resolves to the reply data, so // the dispatch can only continue once the returned promise settles. @@ -313,18 +329,25 @@ function mockDispatch (opts, handler) { if (isPromise(callbackResult)) { callbackResult.then( (resolvedData) => { - mockDispatch.data = { ...mockDispatch.data, ...resolvedData } + if (resolvedData == null || typeof resolvedData !== 'object') { + handler.onResponseError(null, new InvalidArgumentError('reply options callback must return an object')) + return + } + mockDispatch.data = { ...responseDefaults, ...resolvedData } dispatchMockReply(mockDispatches, mockDispatch, key, opts, handler) }, (error) => { - deleteMockDispatch(mockDispatches, key) handler.onResponseError(null, error) } ) return true } - mockDispatch.data = { ...mockDispatch.data, ...callbackResult } + if (callbackResult == null || typeof callbackResult !== 'object') { + throw new InvalidArgumentError('reply options callback must return an object') + } + + mockDispatch.data = { ...responseDefaults, ...callbackResult } } return dispatchMockReply(mockDispatches, mockDispatch, key, opts, handler) @@ -335,12 +358,12 @@ function mockDispatch (opts, handler) { */ function dispatchMockReply (mockDispatches, mockDispatch, key, opts, handler) { // Parse mockDispatch data - const { data: { statusCode, data, headers, trailers, error }, delay } = mockDispatch + const { data: response, delay } = mockDispatch // If specified, trigger dispatch error - if (error !== null) { + if (response.error !== null) { deleteMockDispatch(mockDispatches, key) - handler.onResponseError(null, error) + handler.onResponseError(null, response.error) return true } @@ -375,32 +398,107 @@ function dispatchMockReply (mockDispatches, mockDispatch, key, opts, handler) { } } + let replyOpts = opts + const dispatches = mockDispatches + // Call onRequestStart to allow the handler to receive the controller handler.onRequestStart?.(controller, null) - // Handle the request with a delay if necessary - if (typeof delay === 'number' && delay > 0) { - timer = setTimeout(() => { - timer = null - handleReply(mockDispatches) - }, delay) - } else { - handleReply(mockDispatches) + if (aborted) { + return true + } + + const requestBody = dispatchRequestBody(opts.body, handler, controller, () => aborted) + + if (isPromise(requestBody)) { + requestBody.then((body) => { + if (body === requestAborted) { + return + } + + if (body !== opts.body) { + replyOpts = { ...opts, body } + } + + sendReply() + }, (error) => controller.abort(error)) + return true + } + + if (requestBody === requestAborted) { + return true + } + + if (requestBody !== opts.body) { + replyOpts = { ...opts, body: requestBody } + } + + sendReply() + + function sendReply () { + if (response.callback) { + const { callback, ...responseDefaults } = response + let callbackResult + try { + callbackResult = callback(replyOpts) + } catch (err) { + deleteMockDispatch(mockDispatches, key) + handler.onResponseError(null, err) + return + } + + if (isPromise(callbackResult)) { + callbackResult.then( + (resolvedData) => { + if (resolvedData == null || typeof resolvedData !== 'object') { + handler.onResponseError(null, new InvalidArgumentError('reply options callback must return an object')) + return + } + mockDispatch.data = { ...responseDefaults, ...resolvedData } + handleReply(dispatches, mockDispatch.data) + }, + (err) => { + handler.onResponseError(null, err) + } + ) + return + } + + if (callbackResult == null || typeof callbackResult !== 'object') { + throw new InvalidArgumentError('reply options callback must return an object') + } + + mockDispatch.data = { ...responseDefaults, ...callbackResult } + handleReply(dispatches, mockDispatch.data) + return + } + + // Handle the request with a delay if necessary + if (typeof delay === 'number' && delay > 0) { + timer = setTimeout(() => { + timer = null + handleReply(dispatches) + }, delay) + } else { + handleReply(dispatches) + } } - function handleReply (mockDispatches, _data = data) { + function handleReply (mockDispatches, _response = response) { // Don't send response if the request was aborted if (aborted) { return } + const { statusCode, data, headers, trailers } = _response + // fetch's HeadersList is a 1D string array const optsHeaders = Array.isArray(opts.headers) ? buildHeadersFromArray(opts.headers) : opts.headers - const body = typeof _data === 'function' - ? _data({ ...opts, headers: optsHeaders }) - : _data + const body = typeof data === 'function' + ? data({ ...replyOpts, headers: optsHeaders }) + : data // util.types.isPromise is likely needed for jest. if (isPromise(body)) { @@ -409,7 +507,7 @@ function dispatchMockReply (mockDispatches, mockDispatch, key, opts, handler) { // synchronously throw the error, which breaks some tests. // Rather, we wait for the callback to resolve if it is a // promise, and then re-run handleReply with the new body. - return body.then((newData) => handleReply(mockDispatches, newData)) + return body.then((newData) => handleReply(mockDispatches, { ..._response, data: newData })) } // Check again if aborted after async body resolution @@ -418,8 +516,8 @@ function dispatchMockReply (mockDispatches, mockDispatch, key, opts, handler) { } const responseData = getResponseData(body) - const responseHeaders = generateKeyValues(headers) - const responseTrailers = generateKeyValues(trailers) + const responseHeaders = generateKeyValues(headers ?? {}) + const responseTrailers = generateKeyValues(trailers ?? {}) // Update the controller with response data controller.rawHeaders = responseHeaders @@ -434,6 +532,97 @@ function dispatchMockReply (mockDispatches, mockDispatch, key, opts, handler) { return true } +function dispatchRequestBody (body, handler, controller, isAborted) { + if (typeof handler.onBodySent !== 'function' && typeof handler.onRequestSent !== 'function') { + return body + } + + if (body == null) { + return callOnRequestSent(handler, controller, isAborted) ? body : requestAborted + } + + if (body && typeof body[Symbol.asyncIterator] === 'function') { + return dispatchAsyncIterableBody(body, handler, controller, isAborted) + } + + if (isIterableBody(body)) { + const chunks = [] + + for (const chunk of body) { + if (isAborted()) { + return requestAborted + } + chunks.push(chunk) + if (!callOnBodySent(handler, controller, chunk) || isAborted()) { + return requestAborted + } + } + + return callOnRequestSent(handler, controller, isAborted) ? chunks : requestAborted + } + + if (isAborted()) { + return requestAborted + } + + if (!callOnBodySent(handler, controller, body)) { + return requestAborted + } + + return callOnRequestSent(handler, controller, isAborted) ? body : requestAborted +} + +async function dispatchAsyncIterableBody (body, handler, controller, isAborted) { + const chunks = [] + + for await (const chunk of body) { + if (isAborted()) { + return requestAborted + } + chunks.push(chunk) + if (!callOnBodySent(handler, controller, chunk) || isAborted()) { + return requestAborted + } + } + + if (!callOnRequestSent(handler, controller, isAborted)) { + return requestAborted + } + + return { + async * [Symbol.asyncIterator] () { + yield * chunks + } + } +} + +function callOnBodySent (handler, controller, chunk) { + try { + handler.onBodySent?.(chunk) + return true + } catch (error) { + controller.abort(error) + return false + } +} + +function callOnRequestSent (handler, controller, isAborted) { + try { + handler.onRequestSent?.() + return !isAborted() + } catch (error) { + controller.abort(error) + return false + } +} + +function isIterableBody (body) { + return typeof body !== 'string' && + !Buffer.isBuffer(body) && + !ArrayBuffer.isView(body) && + typeof body[Symbol.iterator] === 'function' +} + function buildMockDispatch () { const agent = this[kMockAgent] const origin = this[kOrigin] diff --git a/deps/undici/src/lib/util/cache.js b/deps/undici/src/lib/util/cache.js index d156731d1b5d..1fac28af5d97 100644 --- a/deps/undici/src/lib/util/cache.js +++ b/deps/undici/src/lib/util/cache.js @@ -148,9 +148,7 @@ function getMalformedRestrictiveDirectiveName (key) { * @param {import('../../types/dispatcher.d.ts').default.DispatchOptions} opts */ function makeCacheKey (opts) { - if (!opts.origin) { - throw new Error('opts.origin is undefined') - } + const origin = opts.origin ? opts.origin.toString() : '' let fullPath = opts.path || '/' @@ -159,7 +157,7 @@ function makeCacheKey (opts) { } return { - origin: opts.origin.toString(), + origin, method: opts.method, path: fullPath, headers: opts.headers diff --git a/deps/undici/src/lib/web/websocket/websocket.js b/deps/undici/src/lib/web/websocket/websocket.js index e473a1bc4917..45dbce1bea93 100644 --- a/deps/undici/src/lib/web/websocket/websocket.js +++ b/deps/undici/src/lib/web/websocket/websocket.js @@ -25,6 +25,9 @@ const { SendQueue } = require('./sender') const { WebsocketFrameSend } = require('./frame') const { channels } = require('../../core/diagnostics') +const kRef = Symbol.for('nodejs.ref') +const kUnref = Symbol.for('nodejs.unref') + function getSocketAddress (socket) { if (typeof socket?.address === 'function') { return socket.address() @@ -68,6 +71,7 @@ class WebSocket extends EventTarget { #bufferedAmount = 0 #protocol = '' #extensions = '' + #refed = true /** @type {SendQueue} */ #sendQueue @@ -194,6 +198,20 @@ class WebSocket extends EventTarget { this.#binaryType = 'blob' } + [kRef] () { + webidl.brandCheck(this, WebSocket) + + this.#refed = true + this.#handler.socket?.ref?.() + } + + [kUnref] () { + webidl.brandCheck(this, WebSocket) + + this.#refed = false + this.#handler.socket?.unref?.() + } + /** * @see https://websockets.spec.whatwg.org/#dom-websocket-close * @param {number|undefined} code @@ -468,6 +486,10 @@ class WebSocket extends EventTarget { // once this happens, the connection is open this.#handler.socket = response.socket + if (!this.#refed) { + this.#handler.socket.unref?.() + } + // Get options from dispatcher options const maxFragments = this.#handler.controller.dispatcher?.webSocketOptions?.maxFragments const maxPayloadSize = this.#handler.controller.dispatcher?.webSocketOptions?.maxPayloadSize diff --git a/deps/undici/src/package-lock.json b/deps/undici/src/package-lock.json index ecc36b1137b0..28d9db8de52e 100644 --- a/deps/undici/src/package-lock.json +++ b/deps/undici/src/package-lock.json @@ -1,12 +1,12 @@ { "name": "undici", - "version": "8.9.0", + "version": "8.10.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "undici", - "version": "8.9.0", + "version": "8.10.0", "license": "MIT", "devDependencies": { "@fastify/busboy": "3.2.0", diff --git a/deps/undici/src/package.json b/deps/undici/src/package.json index f6feb3f0a832..270b572dfc89 100644 --- a/deps/undici/src/package.json +++ b/deps/undici/src/package.json @@ -1,6 +1,6 @@ { "name": "undici", - "version": "8.9.0", + "version": "8.10.0", "description": "An HTTP/1.1 client, written from scratch for Node.js", "homepage": "https://undici.nodejs.org", "bugs": { diff --git a/deps/undici/src/types/client.d.ts b/deps/undici/src/types/client.d.ts index e3b121962ef0..064d3d69f2c1 100644 --- a/deps/undici/src/types/client.d.ts +++ b/deps/undici/src/types/client.d.ts @@ -1,10 +1,15 @@ import { URL } from 'node:url' +import { SessionOptions } from 'node:http2' import Dispatcher from './dispatcher' import buildConnector from './connector' import TClientStats from './client-stats' type ClientConnectOptions = Omit, 'origin'> +// TODO: Pendings +// 1. Reflect this on Client instantiation +// 2. Client H2 should use this namespaced options instead. + /** * A basic HTTP/1.1 client, mapped on top a single TCP/TLS connection. Pipelining is disabled by default. */ @@ -87,23 +92,31 @@ export declare namespace Client { /** * @description Dictates the maximum number of concurrent streams for a single H2 session. It can be overridden by a SETTINGS remote frame. * @default 100 + * @deprecated Use h2Options.maxConcurrentStreams instead */ maxConcurrentStreams?: number; /** * @description Sets the HTTP/2 stream-level flow-control window size (SETTINGS_INITIAL_WINDOW_SIZE). * @default 262144 + * @deprecated Use h2Options.settings.initialWindowSize instead */ initialWindowSize?: number; /** * @description Sets the HTTP/2 connection-level flow-control window size (ClientHttp2Session.setLocalWindowSize). * @default 524288 + * @deprecated Use h2Options.connectionWindowSize instead */ connectionWindowSize?: number; /** * @description Time interval between PING frames dispatch * @default 60000 + * @deprecated Use h2Options.connectionWindowSize instead */ pingInterval?: number; + /** + * @description HTTP/2 configuration options + */ + h2Options?: Client.H2Options; } export interface SocketInfo { localAddress?: string @@ -129,6 +142,33 @@ export declare namespace Client { */ maxPayloadSize?: number; } + + export interface H2Options extends Omit { + /** + * @description Sets the HTTP/2 connection-level flow-control window size (ClientHttp2Session.setLocalWindowSize). + * @default 524288 + */ + connectionWindowSize?: number; + /** + * @description Time interval between PING frames dispatch + * @default 60000 + */ + pingInterval?: number; + /** + * @description Dictates the maximum number of concurrent streams for a single H2 session. It can be overridden by a SETTINGS remote frame. + * @default 100 + */ + maxConcurrentStreams?: number; + /** + * @description Enable support for H2C (plain text) + * @default false + */ + useH2c?: boolean; + /** + * @description SETTINGS frame object. Default to 'node:http2' defaults + */ + settings?: Omit + } } export default Client diff --git a/deps/undici/undici.js b/deps/undici/undici.js index c0505480ffe9..bb398a0686ce 100644 --- a/deps/undici/undici.js +++ b/deps/undici/undici.js @@ -574,6 +574,7 @@ var require_symbols = __commonJS({ kCounter: /* @__PURE__ */ Symbol("socket request counter"), kMaxResponseSize: /* @__PURE__ */ Symbol("max response size"), kHTTP2Session: /* @__PURE__ */ Symbol("http2Session"), + kHTTP2Options: /* @__PURE__ */ Symbol("http2 options"), kHTTP2SessionState: /* @__PURE__ */ Symbol("http2Session state"), kRetryHandlerDefaultRetry: /* @__PURE__ */ Symbol("retry agent default retry"), kConstruct: /* @__PURE__ */ Symbol("constructable"), @@ -3266,14 +3267,26 @@ var require_connect = __commonJS({ } else { assert(!httpSocket, "httpSocket can only be sent on TLS update"); port = port || 80; - socket = net.connect({ + const connectOptions = { highWaterMark: 64 * 1024, // Same as nodejs fs streams. ...options, localAddress, port, host: hostname - }); + }; + const family = net.isIP(hostname); + if (family !== 0 && servername && servername !== hostname) { + connectOptions.host = servername; + connectOptions.lookup = (_hostname, lookupOptions, cb) => { + if (lookupOptions.all) { + cb(null, [{ address: hostname, family }]); + } else { + cb(null, hostname, family); + } + }; + } + socket = net.connect(connectOptions); if (useH2c === true) { socket.alpnProtocol = "h2"; } @@ -7843,7 +7856,7 @@ var require_client_h1 = __commonJS({ __name(onSocketClose, "onSocketClose"); function clearIdleSocketValidation(socket) { if (socket[kIdleSocketValidationTimeout]) { - clearImmediate(socket[kIdleSocketValidationTimeout]); + clearTimeout(socket[kIdleSocketValidationTimeout]); socket[kIdleSocketValidationTimeout] = null; } socket[kIdleSocketValidation] = 0; @@ -7851,13 +7864,13 @@ var require_client_h1 = __commonJS({ __name(clearIdleSocketValidation, "clearIdleSocketValidation"); function scheduleIdleSocketValidation(client, socket) { socket[kIdleSocketValidation] = 1; - socket[kIdleSocketValidationTimeout] = setImmediate(() => { + socket[kIdleSocketValidationTimeout] = setTimeout(() => { socket[kIdleSocketValidationTimeout] = null; socket[kIdleSocketValidation] = 2; if (client[kSocket] === socket && !socket.destroyed) { client[kResume](); } - }); + }, 0); socket[kIdleSocketValidationTimeout].unref?.(); } __name(scheduleIdleSocketValidation, "scheduleIdleSocketValidation"); @@ -8394,10 +8407,7 @@ var require_client_h2 = __commonJS({ kStrictContentLength, kOnError, kMaxConcurrentStreams, - kPingInterval, kHTTP2Session, - kHTTP2InitialWindowSize, - kHTTP2ConnectionWindowSize, kHostAuthority, kResume, kSize, @@ -8409,7 +8419,8 @@ var require_client_h2 = __commonJS({ kEnableConnectProtocol, kRemoteSettings, kHTTP2Stream, - kHTTP2SessionState + kHTTP2SessionState, + kHTTP2Options } = require_symbols(); var { channels } = require_diagnostics(); var kOpenStreams = /* @__PURE__ */ Symbol("open streams"); @@ -8418,6 +8429,9 @@ var require_client_h2 = __commonJS({ var kRequestStreamCleanup = /* @__PURE__ */ Symbol("request stream cleanup"); var kRequestStreamState = /* @__PURE__ */ Symbol("request stream state"); var kReceivedGoAway = /* @__PURE__ */ Symbol("received goaway"); + var kGoAwayReplayAttempts = /* @__PURE__ */ Symbol("goaway replay attempts"); + var kRefusedStreamRetry = /* @__PURE__ */ Symbol("refused stream retry"); + var MAX_GOAWAY_REPLAY_ATTEMPTS = 1; var extractBody; var http2; try { @@ -8523,11 +8537,17 @@ var require_client_h2 = __commonJS({ } } __name(completeRequest, "completeRequest"); - function canRetryRequestAfterGoAway(request) { + function canReplayRequest(request) { const { body } = request; return body == null || util.isBuffer(body) || util.isBlobLike(body); } - __name(canRetryRequestAfterGoAway, "canRetryRequestAfterGoAway"); + __name(canReplayRequest, "canReplayRequest"); + function registerGoAwayRefusal(request) { + const attempts = (request[kGoAwayReplayAttempts] ?? 0) + 1; + request[kGoAwayReplayAttempts] = attempts; + return attempts <= MAX_GOAWAY_REPLAY_ATTEMPTS; + } + __name(registerGoAwayRefusal, "registerGoAwayRefusal"); function closeStream(stream, code = NGHTTP2_REFUSED_STREAM) { if (stream != null && !stream.destroyed && !stream.closed) { try { @@ -8540,16 +8560,32 @@ var require_client_h2 = __commonJS({ function detachRequestStreamForClose(request) { const stream = request[kRequestStream]; clearRequestStream(request); + severRequestStream(stream); return stream; } __name(detachRequestStreamForClose, "detachRequestStreamForClose"); + function severRequestStream(stream) { + if (stream == null || stream[kRequestStreamState] == null) { + return; + } + stream[kRequestStreamState] = null; + stream.off("close", completeRequestStream); + stream.off("close", onUpgradeStreamClose); + if (stream[kHTTP2Session] != null) { + closeStreamSession(stream); + } + if (!stream.destroyed && !stream.closed) { + stream.once("error", noop); + } + } + __name(severRequestStream, "severRequestStream"); function connectH2(client, socket) { client[kSocket] = socket; - const http2InitialWindowSize = client[kHTTP2InitialWindowSize]; - const http2ConnectionWindowSize = client[kHTTP2ConnectionWindowSize]; + const http2InitialWindowSize = client[kHTTP2Options].sessionOptions?.initialWindowSize; + const http2ConnectionWindowSize = client[kHTTP2Options].connectionWindowSize; const session = http2.connect(client[kUrl], { createConnection: /* @__PURE__ */ __name(() => socket, "createConnection"), - peerMaxConcurrentStreams: client[kMaxConcurrentStreams], + peerMaxConcurrentStreams: client[kHTTP2Options].maxConcurrentStreams, settings: { // TODO(metcoder95): add support for PUSH enablePush: false, @@ -8562,13 +8598,16 @@ var require_client_h2 = __commonJS({ session[kSocket] = socket; session[kHTTP2SessionState] = { idleTimeout: null, + // Armed while the peer advertises MAX_CONCURRENT_STREAMS = 0 and we have + // work that cannot start. See setNoStreamsTimeout. + noStreamsTimeout: null, // Sockets start out ref'd. Session ref/unref proxies to the socket, so a // single cached flag lets us skip redundant uv ref/unref calls, provided // every ref/unref of the session or its socket goes through // refH2Session/unrefH2Session. refed: true, ping: { - interval: client[kPingInterval] === 0 ? null : setInterval(onHttp2SendPing, client[kPingInterval], session).unref() + interval: client[kHTTP2Options].pingInterval === 0 ? null : setInterval(onHttp2SendPing, client[kHTTP2Options].pingInterval, session).unref() } }; session[kReceivedGoAway] = false; @@ -8676,9 +8715,52 @@ var require_client_h2 = __commonJS({ } else { clearHttp2IdleTimeout(session); } + if (client[kMaxConcurrentStreams] === 0 && client[kRunning] === 0 && client[kPending] > 0) { + setNoStreamsTimeout(session); + } else { + clearNoStreamsTimeout(session); + } } } __name(resumeH2, "resumeH2"); + function clearNoStreamsTimeout(session) { + const state = session[kHTTP2SessionState]; + if (state?.noStreamsTimeout != null) { + clearTimeout(state.noStreamsTimeout); + state.noStreamsTimeout = null; + } + } + __name(clearNoStreamsTimeout, "clearNoStreamsTimeout"); + function setNoStreamsTimeout(session) { + const client = session[kClient]; + const state = session[kHTTP2SessionState]; + const timeout = client[kHeadersTimeout]; + if (!timeout || state.noStreamsTimeout != null) { + return; + } + state.noStreamsTimeout = setTimeout(onNoStreamsTimeout, timeout, session).unref(); + } + __name(setNoStreamsTimeout, "setNoStreamsTimeout"); + function onNoStreamsTimeout(session) { + const client = session[kClient]; + const state = session[kHTTP2SessionState]; + state.noStreamsTimeout = null; + if (client[kHTTP2Session] !== session || client[kMaxConcurrentStreams] !== 0 || client[kRunning] !== 0 || client[kPending] === 0) { + return; + } + const err = new HeadersTimeoutError( + `HTTP/2: server did not accept a new stream within ${client[kHeadersTimeout]}` + ); + const requests = client[kQueue].splice(client[kPendingIdx]); + for (let i = 0; i < requests.length; i++) { + if (requests[i] != null) { + util.errorRequest(client, requests[i], err); + } + } + session[kError] = err; + resetHttp2Session(session, err); + } + __name(onNoStreamsTimeout, "onNoStreamsTimeout"); function clearHttp2IdleTimeout(session) { const state = session[kHTTP2SessionState]; if (state?.idleTimeout != null) { @@ -8794,7 +8876,7 @@ var require_client_h2 = __commonJS({ const request = client[kQueue][i]; if (request != null) { streamsToClose.push(detachRequestStreamForClose(request)); - if (canRetryRequestAfterGoAway(request)) { + if (canReplayRequest(request) && registerGoAwayRefusal(request)) { retriableRequests.push(request); } else { util.errorRequest(client, request, err); @@ -8815,6 +8897,7 @@ var require_client_h2 = __commonJS({ client[kHTTP2Session] = null; } clearHttp2IdleTimeout(this); + clearNoStreamsTimeout(this); if (!this.closed && !this.destroyed) { this.close(); } @@ -8832,6 +8915,7 @@ var require_client_h2 = __commonJS({ client[kHTTP2Session] = null; } clearHttp2IdleTimeout(this); + clearNoStreamsTimeout(this); if (state.ping.interval != null) { clearInterval(state.ping.interval); state.ping.interval = null; @@ -8915,6 +8999,12 @@ var require_client_h2 = __commonJS({ releaseRequestStream(this); if (state.pendingEnd && !state.request.aborted && !state.request.completed) { state.request.onResponseEnd(state.trailers || {}); + } else if (!state.request.aborted && !state.request.completed) { + util.errorRequest( + state.client, + state.request, + new InformationalError("HTTP/2: stream closed before the response was complete") + ); } finalizeRequest(state); closeStreamSession(this); @@ -9333,6 +9423,21 @@ var require_client_h2 = __commonJS({ } } __name(onEnd, "onEnd"); + function retryRefusedStream(stream, state) { + const { client, request } = state; + if (state.responseReceived || request.aborted || request.completed || request[kRefusedStreamRetry] || !canReplayRequest(request)) { + return false; + } + request[kRefusedStreamRetry] = true; + detachRequestStreamForClose(request); + state.stream = null; + state.requestFinalized = true; + completeRequest(client, request); + client[kQueue].splice(client[kPendingIdx], 0, request); + client[kResume](); + return true; + } + __name(retryRefusedStream, "retryRefusedStream"); function onError(err) { const stream = this; const state = stream[kRequestStreamState]; @@ -9340,6 +9445,12 @@ var require_client_h2 = __commonJS({ return; } stream.off("error", onError); + if (typeof stream.rstCode === "number" && stream.rstCode !== NGHTTP2_NO_ERROR) { + err.http2ErrorCode = stream.rstCode; + } + if (stream.rstCode === NGHTTP2_REFUSED_STREAM && retryRefusedStream(stream, state)) { + return; + } state.abort(err); } __name(onError, "onError"); @@ -9633,10 +9744,8 @@ var require_client = __commonJS({ kHTTPContext, kMaxConcurrentStreams, kHostAuthority, - kHTTP2InitialWindowSize, - kHTTP2ConnectionWindowSize, kResume, - kPingInterval + kHTTP2Options } = require_symbols(); var connectH1 = require_client_h1(); var connectH2 = require_client_h2(); @@ -9650,6 +9759,15 @@ var require_client = __commonJS({ return client[kPipelining] ?? client[kHTTPContext]?.defaultPipelining ?? 1; } __name(getPipelining, "getPipelining"); + var h2NamespaceOptsWarning = false; + function emitH2OptionsNamespaceWarning(optName) { + if (h2NamespaceOptsWarning === true) return; + process.emitWarning(`Use h2Options.${optName} instead. ${optName} for H2 will be deprecated in future major.`, { + code: "UNDICI-H2-OPTIONS" + }); + h2NamespaceOptsWarning = true; + } + __name(emitH2OptionsNamespaceWarning, "emitH2OptionsNamespaceWarning"); function getMaxConcurrent(client) { if (client[kHTTPContext]?.version === "h2") { return client[kMaxConcurrentStreams]; @@ -9697,7 +9815,8 @@ var require_client = __commonJS({ initialWindowSize, connectionWindowSize, pingInterval, - webSocket + webSocket, + h2Options } = {}) { if (keepAlive !== void 0) { throw new InvalidArgumentError("unsupported keepAlive, use pipelining=0 instead"); @@ -9760,20 +9879,45 @@ var require_client = __commonJS({ if (allowH2 != null && typeof allowH2 !== "boolean") { throw new InvalidArgumentError("allowH2 must be a valid boolean value"); } - if (maxConcurrentStreams != null && (typeof maxConcurrentStreams !== "number" || maxConcurrentStreams < 1)) { - throw new InvalidArgumentError("maxConcurrentStreams must be a positive integer, greater than 0"); - } - if (useH2c != null && typeof useH2c !== "boolean") { - throw new InvalidArgumentError("useH2c must be a valid boolean value"); - } - if (initialWindowSize != null && (!Number.isInteger(initialWindowSize) || initialWindowSize < 1)) { - throw new InvalidArgumentError("initialWindowSize must be a positive integer, greater than 0"); - } - if (connectionWindowSize != null && (!Number.isInteger(connectionWindowSize) || connectionWindowSize < 1)) { - throw new InvalidArgumentError("connectionWindowSize must be a positive integer, greater than 0"); - } - if (pingInterval != null && (typeof pingInterval !== "number" || !Number.isInteger(pingInterval) || pingInterval < 0)) { - throw new InvalidArgumentError("pingInterval must be a positive integer, greater or equal to 0"); + if (allowH2 !== false) { + if (h2Options != null) { + if (h2Options.useH2c != null && typeof h2Options.useH2c !== "boolean") { + throw new InvalidArgumentError("h2Options.useH2c must be a valid boolean value"); + } + if (h2Options.settings?.initialWindowSize != null && (!Number.isInteger(h2Options.settings.initialWindowSize) || h2Options.settings.initialWindowSize < 1)) { + throw new InvalidArgumentError("h2Options.settings.initialWindowSize must be a positive integer, greater than 0"); + } + if (h2Options.maxConcurrentStreams != null && (!Number.isInteger(h2Options.connectionWindowSize) || h2Options.maxConcurrentStreams < 1)) { + throw new InvalidArgumentError("h2Options.maxConcurrentStreams must be a positive integer, greater than 0"); + } + if (h2Options.connectionWindowSize != null && (!Number.isInteger(h2Options.connectionWindowSize) || h2Options.connectionWindowSize < 1)) { + throw new InvalidArgumentError("h2Options.connectionWindowSize must be a positive integer, greater than 0"); + } + if (h2Options.pingInterval != null && (typeof h2Options.pingInterval !== "number" || !Number.isInteger(h2Options.pingInterval) || h2Options.pingInterval < 0)) { + throw new InvalidArgumentError("h2Options.pingInterval must be a positive integer, greater or equal to 0"); + } + } else { + if (useH2c != null && typeof useH2c !== "boolean") { + emitH2OptionsNamespaceWarning("useH2c"); + throw new InvalidArgumentError("useH2c must be a valid boolean value"); + } + if (maxConcurrentStreams != null && (typeof maxConcurrentStreams !== "number" || maxConcurrentStreams < 1)) { + emitH2OptionsNamespaceWarning("maxConcurrentStreams"); + throw new InvalidArgumentError("maxConcurrentStreams must be a positive integer, greater than 0"); + } + if (initialWindowSize != null && (!Number.isInteger(initialWindowSize) || initialWindowSize < 1)) { + emitH2OptionsNamespaceWarning("initialWindowSize"); + throw new InvalidArgumentError("initialWindowSize must be a positive integer, greater than 0"); + } + if (connectionWindowSize != null && (!Number.isInteger(connectionWindowSize) || connectionWindowSize < 1)) { + emitH2OptionsNamespaceWarning("connectionWindowSize"); + throw new InvalidArgumentError("connectionWindowSize must be a positive integer, greater than 0"); + } + if (pingInterval != null && (typeof pingInterval !== "number" || !Number.isInteger(pingInterval) || pingInterval < 0)) { + emitH2OptionsNamespaceWarning("pingInterval"); + throw new InvalidArgumentError("pingInterval must be a positive integer, greater or equal to 0"); + } + } } super({ webSocket }); if (typeof connect2 !== "function") { @@ -9781,8 +9925,8 @@ var require_client = __commonJS({ ...tls, maxCachedSessions, allowH2, - useH2c, socketPath, + useH2c: h2Options?.useH2c ?? useH2c, timeout: connectTimeout, ...typeof autoSelectFamily === "boolean" ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : void 0, ...connect2 @@ -9817,10 +9961,21 @@ var require_client = __commonJS({ this[kClosedResolve] = null; this[kMaxResponseSize] = maxResponseSize > -1 ? maxResponseSize : -1; this[kHTTPContext] = null; - this[kMaxConcurrentStreams] = maxConcurrentStreams != null ? maxConcurrentStreams : 100; - this[kHTTP2InitialWindowSize] = initialWindowSize != null ? initialWindowSize : 262144; - this[kHTTP2ConnectionWindowSize] = connectionWindowSize != null ? connectionWindowSize : 524288; - this[kPingInterval] = pingInterval != null ? pingInterval : 6e4; + this[kHTTP2Options] = { + pingInterval: h2Options?.pingInterval ?? pingInterval ?? 6e4, + connectionWindowSize: h2Options?.connectionWindowSize ?? connectionWindowSize ?? 524288, + maxConcurrentStreams: h2Options?.maxConcurrentStreams ?? maxConcurrentStreams ?? 100, + // Max peerConcurrentStreams for a Node h2 server + sessionOptions: { + // HTTP/2 window sizes are set to higher defaults than Node.js core for better performance: + // - initialWindowSize: 262144 (256KB) vs Node.js default 65535 (64KB - 1) + // Allows more data to be sent before requiring acknowledgment, improving throughput + // especially on high-latency networks. This matches common production HTTP/2 servers. + // - connectionWindowSize: 524288 (512KB) vs Node.js default (none set) + // Provides better flow control for the entire connection across multiple streams. + initialWindowSize: h2Options?.initialWindowSize ?? initialWindowSize ?? 262144 + } + }; this[kQueue] = []; this[kRunningIdx] = 0; this[kPendingIdx] = 0; @@ -10110,6 +10265,7 @@ var require_client = __commonJS({ return; } if (!client[kHTTPContext]) { + client[kServerName] = request.servername; connect(client); return; } @@ -11076,7 +11232,7 @@ var require_socks5_proxy_agent = __commonJS({ var DispatcherBase = require_dispatcher_base(); var { InvalidArgumentError } = require_errors(); var { Socks5Client, STATES } = require_socks5_client(); - var { kDispatch, kClose, kDestroy } = require_symbols(); + var { kBusy, kConnected, kDispatch, kClose, kDestroy } = require_symbols(); var Pool = require_pool(); var buildConnector = require_connect(); var { debuglog } = require("node:util"); @@ -11237,6 +11393,17 @@ var require_socks5_proxy_agent = __commonJS({ }, "connect") }); this[kPools].set(originKey, pool); + const closePoolIfUnused = /* @__PURE__ */ __name(() => { + if (this[kPools].get(originKey) !== pool || pool[kConnected] > 0 || pool[kBusy]) { + return; + } + this[kPools].delete(originKey); + if (!pool.destroyed) { + pool.close(); + } + }, "closePoolIfUnused"); + pool.on("disconnect", closePoolIfUnused); + pool.on("connectionError", closePoolIfUnused); } return pool[kDispatch](opts, handler); } catch (err) { @@ -11638,7 +11805,7 @@ var require_env_http_proxy_agent = __commonJS({ } #getProxyAgentForUrl(url) { let { protocol, host: hostname, port } = url; - hostname = hostname.replace(/:\d*$/, "").toLowerCase(); + hostname = hostname.replace(/:\d*$/, "").replace(/^\[(.+)\]$/, "$1").toLowerCase(); port = Number.parseInt(port, 10) || DEFAULT_PORTS[protocol] || 0; if (!this.#shouldProxy(hostname, port)) { return this[kNoProxyAgent]; @@ -11681,11 +11848,22 @@ var require_env_http_proxy_agent = __commonJS({ if (!entry) { continue; } - const parsed = entry.match(/^(.+):(\d+)$/); + let hostname, port; + const ipv6WithPort = entry.match(/^\[(.+)\]:(\d+)$/); + if (ipv6WithPort) { + hostname = ipv6WithPort[1]; + port = Number.parseInt(ipv6WithPort[2], 10); + } else { + const unbracketed = entry.replace(/^\[(.+)\]$/, "$1"); + const colonCount = (unbracketed.match(/:/g) || []).length; + const parsed = colonCount === 1 && unbracketed.match(/^(.+):(\d+)$/); + hostname = parsed ? parsed[1] : unbracketed; + port = parsed ? Number.parseInt(parsed[2], 10) : 0; + } noProxyEntries.push({ // strip leading dot or asterisk with dot - hostname: (parsed ? parsed[1] : entry).replace(/^\*?\./, "").toLowerCase(), - port: parsed ? Number.parseInt(parsed[2], 10) : 0 + hostname: hostname.replace(/^\*?\./, "").toLowerCase(), + port }); } this.#noProxyValue = noProxyValue; @@ -16062,6 +16240,8 @@ var require_websocket = __commonJS({ var { SendQueue } = require_sender(); var { WebsocketFrameSend } = require_frame(); var { channels } = require_diagnostics(); + var kRef = /* @__PURE__ */ Symbol.for("nodejs.ref"); + var kUnref = /* @__PURE__ */ Symbol.for("nodejs.unref"); function getSocketAddress(socket) { if (typeof socket?.address === "function") { return socket.address(); @@ -16085,6 +16265,7 @@ var require_websocket = __commonJS({ #bufferedAmount = 0; #protocol = ""; #extensions = ""; + #refed = true; /** @type {SendQueue} */ #sendQueue; /** @type {Handler} */ @@ -16167,6 +16348,16 @@ var require_websocket = __commonJS({ this.#handler.readyState = _WebSocket.CONNECTING; this.#binaryType = "blob"; } + [kRef]() { + webidl.brandCheck(this, _WebSocket); + this.#refed = true; + this.#handler.socket?.ref?.(); + } + [kUnref]() { + webidl.brandCheck(this, _WebSocket); + this.#refed = false; + this.#handler.socket?.unref?.(); + } /** * @see https://websockets.spec.whatwg.org/#dom-websocket-close * @param {number|undefined} code @@ -16328,6 +16519,9 @@ var require_websocket = __commonJS({ */ #onConnectionEstablished(response, parsedExtensions) { this.#handler.socket = response.socket; + if (!this.#refed) { + this.#handler.socket.unref?.(); + } const maxFragments = this.#handler.controller.dispatcher?.webSocketOptions?.maxFragments; const maxPayloadSize = this.#handler.controller.dispatcher?.webSocketOptions?.maxPayloadSize; const parser = new ByteParser(this.#handler, parsedExtensions, { @@ -17247,7 +17441,6 @@ var require_readable = __commonJS({ var kContentLength = /* @__PURE__ */ Symbol("kContentLength"); var kUsed = /* @__PURE__ */ Symbol("kUsed"); var kBytesRead = /* @__PURE__ */ Symbol("kBytesRead"); - var kPreservedBuffer = /* @__PURE__ */ Symbol("kPreservedBuffer"); var noop = /* @__PURE__ */ __name(() => { }, "noop"); var BodyReadable = class extends Readable { @@ -17489,21 +17682,6 @@ var require_readable = __commonJS({ */ setEncoding(encoding) { if (Buffer.isEncoding(encoding)) { - const state = this._readableState; - const buffer = state.buffer; - if (buffer && state.length > 0) { - const bufferIndex = state.bufferIndex ?? 0; - const preserved = []; - const source = typeof buffer.slice === "function" ? buffer.slice(bufferIndex) : buffer; - for (const data of source) { - if (Buffer.isBuffer(data)) { - preserved.push(data); - } - } - if (preserved.length > 0) { - this[kPreservedBuffer] = (this[kPreservedBuffer] || []).concat(preserved); - } - } super.setEncoding(encoding); } return this; @@ -17557,13 +17735,7 @@ var require_readable = __commonJS({ return; } const { _readableState: state } = consume2.stream; - const preserved = consume2.stream[kPreservedBuffer]; - if (preserved && preserved.length > 0) { - for (const chunk of preserved) { - consumePush(consume2, chunk); - } - consume2.stream[kPreservedBuffer] = null; - } else if (state.bufferIndex) { + if (state.bufferIndex) { const start = state.bufferIndex; const end = state.buffer.length; for (let n = start; n < end; n++) { @@ -17574,13 +17746,17 @@ var require_readable = __commonJS({ consumePush(consume2, chunk); } } + const decoder = state.decoder; + if (decoder != null && decoder.lastNeed > 0) { + consumePush(consume2, Buffer.from(decoder.lastChar.subarray(0, decoder.lastTotal - decoder.lastNeed))); + } if (state.endEmitted) { - consumeEnd(this[kConsume], this._readableState.encoding); - } else { - consume2.stream.on("end", function() { - consumeEnd(this[kConsume], this._readableState.encoding); - }); + consumeEnd(consume2, state.encoding); + return; } + consume2.stream.on("end", function() { + consumeEnd(this[kConsume], this._readableState.encoding); + }); consume2.stream.resume(); while (consume2.stream.read() != null) { } @@ -17641,6 +17817,9 @@ var require_readable = __commonJS({ if (consume2.body === null) { return; } + if (typeof chunk === "string") { + chunk = Buffer.from(chunk, consume2.stream._readableState.encoding); + } consume2.length += chunk.length; consume2.body.push(chunk); } diff --git a/doc/api/assert.md b/doc/api/assert.md index 1a99709767ef..c33f4ba82336 100644 --- a/doc/api/assert.md +++ b/doc/api/assert.md @@ -317,7 +317,7 @@ const assert2 = new Assert({ skipPrototype: true }); assert2.deepStrictEqual(foo, bar); // OK ``` -When destructured, methods lose access to the instance's `this` context and revert to default assertion behavior +When destructured, methods lose access to the instance's `this` context and revert to the default assertion behavior (diff: 'simple', non-strict mode). To maintain custom options when using destructured methods, avoid destructuring and call methods directly on the instance. @@ -423,8 +423,8 @@ are also recursively evaluated by the following rules. ### Comparison details * Primitive values are compared with the [`==` operator][], - with the exception of {NaN}. It is treated as being identical in case - both sides are {NaN}. + except for {NaN}, which is treated as identical when both + sides are {NaN}. * [Type tags][Object.prototype.toString()] of objects should be the same. * Only [enumerable "own" properties][] are considered. * Object constructors are compared when available. @@ -938,7 +938,7 @@ error messages as expressive as possible. If specified, `error` can be a [`Class`][], {RegExp} or a validation function. See [`assert.throws()`][] for more details. -Besides the async nature to await the completion behaves identically to +Aside from asynchronously awaiting completion, it behaves identically to [`assert.doesNotThrow()`][]. ```mjs diff --git a/doc/api/buffer.md b/doc/api/buffer.md index 64802f07f76a..4fd34498c050 100644 --- a/doc/api/buffer.md +++ b/doc/api/buffer.md @@ -791,11 +791,14 @@ data that might not have been allocated for `Buffer`s. A `TypeError` will be thrown if `size` is not a number. -### Static method: `Buffer.allocUnsafe(size)` +### Static method: `Buffer.allocUnsafe(size[, alignment])` * `size` {integer} The desired length of the new `Buffer`. +* `alignment` {integer} If given, the memory backing the new `Buffer` will start + at an address that is a multiple of `alignment`. Must be a power of two no + larger than `2 ** 30`. See [Aligned allocations][]. * Returns: {Buffer} Allocates a new `Buffer` of `size` bytes. If `size` is larger than @@ -865,11 +871,14 @@ pool, while `Buffer.allocUnsafe(size).fill(fill)` _will_ use the internal difference is subtle but can be important when an application requires the additional performance that [`Buffer.allocUnsafe()`][] provides. -### Static method: `Buffer.allocUnsafeSlow(size)` +### Static method: `Buffer.allocUnsafeSlow(size[, alignment])` * `size` {integer} The desired length of the new `Buffer`. +* `alignment` {integer} If given, the memory backing the new `Buffer` will start + at an address that is a multiple of `alignment`. Must be a power of two no + larger than `2 ** 30`. See [Aligned allocations][]. * Returns: {Buffer} Allocates a new `Buffer` of `size` bytes. If `size` is larger than @@ -5317,6 +5329,11 @@ npx codemod@latest @nodejs/buffer-atob-btoa added: - v19.6.0 - v18.15.0 +changes: + - version: REPLACEME + pr-url: https://github.com/nodejs/node/pull/64504 + description: Detached `ArrayBuffer`s and views backed by them are treated + as empty. --> * `input` {Buffer | ArrayBuffer | TypedArray} The input to validate. @@ -5325,7 +5342,7 @@ added: This function returns `true` if `input` contains only valid ASCII-encoded data, including the case in which `input` is empty. -Throws if the `input` is a detached array buffer. +A detached `ArrayBuffer`, or a `TypedArray` backed by one, is treated as empty. ### `buffer.isUtf8(input)` @@ -5333,6 +5350,11 @@ Throws if the `input` is a detached array buffer. added: - v19.4.0 - v18.14.0 +changes: + - version: REPLACEME + pr-url: https://github.com/nodejs/node/pull/64504 + description: Detached `ArrayBuffer`s and views backed by them are treated + as empty. --> * `input` {Buffer | ArrayBuffer | TypedArray} The input to validate. @@ -5341,7 +5363,7 @@ added: This function returns `true` if `input` contains only valid UTF-8-encoded data, including the case in which `input` is empty. -Throws if the `input` is a detached array buffer. +A detached `ArrayBuffer`, or a `TypedArray` backed by one, is treated as empty. ### `buffer.INSPECT_MAX_BYTES` @@ -5598,16 +5620,92 @@ While there are clear performance advantages to using [`Buffer.allocUnsafe()`][], extra care _must_ be taken in order to avoid introducing security vulnerabilities into an application. +### Aligned allocations + +Some operating system interfaces require the memory they operate on to be +aligned, and on some hardware alignment is merely faster. The most common +example of the former is unbuffered ("direct") file I/O, which on Linux requires +the buffer address, the file offset and the transfer length to all be multiples +of the logical block size of the underlying device: + +```mjs +import { open } from 'node:fs/promises'; +import { constants } from 'node:fs'; +import { Buffer } from 'node:buffer'; + +const blockSize = 4096; + +// The buffer address must be block-aligned for O_DIRECT to accept it. +const buf = Buffer.allocUnsafeSlow(blockSize, blockSize); + +const file = await open('/dev/sda', constants.O_RDONLY | constants.O_DIRECT); +try { + await file.read(buf, 0, blockSize, 0); +} finally { + await file.close(); +} +``` + +```cjs +const fs = require('node:fs'); +const { Buffer } = require('node:buffer'); + +const blockSize = 4096; + +// The buffer address must be block-aligned for O_DIRECT to accept it. +const buf = Buffer.allocUnsafeSlow(blockSize, blockSize); + +const flags = fs.constants.O_RDONLY | fs.constants.O_DIRECT; +fs.open('/dev/sda', flags, (err, fd) => { + if (err) throw err; + fs.read(fd, buf, 0, blockSize, 0, (err) => { + fs.close(fd, () => {}); + if (err) throw err; + }); +}); +``` + +Alignment can also be worth requesting purely for performance, even when no +interface demands it. Aligning a hot `Buffer` to the cache line size (64 bytes on +most contemporary CPUs) keeps it from straddling one more cache line than it +needs to, so that a small structure is fetched with one cache miss instead of +two, and page-aligned (4096 bytes) allocations similarly help interfaces that map +or pin memory. These are micro-optimizations: measure before reaching for them, +since the extra bytes are not free. + +Because the address of a `Buffer`'s memory cannot be chosen directly, extra bytes +have to be allocated or skipped to reach an aligned address. +[`Buffer.allocUnsafeSlow()`][] over-allocates up to `alignment - 1` bytes and +positions the returned `Buffer` at the first suitably aligned byte within them. +[`Buffer.allocUnsafe()`][] instead pads its offset into the shared internal pool, +whose start is always aligned to 64 bytes, and only falls back to an allocation +of its own when `alignment` is larger than that. Either way, +[`buf.byteOffset`][] is usually not 0 and [`buf.buffer`][] is larger than `size`, +so code that reaches past the `Buffer` into its underlying `ArrayBuffer` must +take the offset into account, as it must for pooled `Buffer`s. + +The alignment is a property of the returned `Buffer` and is preserved for its +whole lifetime, but it is not inherited by other views: [`buf.subarray`][], +[`buf.slice()`][] and `structuredClone()` may all produce unaligned `Buffer`s. + +Alignment also does not survive being captured in a startup snapshot: memory does +not keep its address across serialization, so a `Buffer` allocated while +[`--build-snapshot`][] is in effect is not aligned in the deserialized process. +Allocate inside a [`v8.startupSnapshot.setDeserializeMainFunction()`][] callback, +or after startup, if the alignment has to hold at run time. + [ASCII]: https://en.wikipedia.org/wiki/ASCII +[Aligned allocations]: #aligned-allocations [Base64]: https://en.wikipedia.org/wiki/Base64 [ISO-8859-1]: https://en.wikipedia.org/wiki/ISO-8859-1 [RFC 4648, Section 5]: https://tools.ietf.org/html/rfc4648#section-5 [UTF-16]: https://en.wikipedia.org/wiki/UTF-16 [UTF-8]: https://en.wikipedia.org/wiki/UTF-8 [WHATWG Encoding Standard]: https://encoding.spec.whatwg.org/ +[`--build-snapshot`]: cli.md#--build-snapshot [`Buffer.alloc()`]: #static-method-bufferallocsize-fill-encoding -[`Buffer.allocUnsafe()`]: #static-method-bufferallocunsafesize -[`Buffer.allocUnsafeSlow()`]: #static-method-bufferallocunsafeslowsize +[`Buffer.allocUnsafe()`]: #static-method-bufferallocunsafesize-alignment +[`Buffer.allocUnsafeSlow()`]: #static-method-bufferallocunsafeslowsize-alignment [`Buffer.concat()`]: #static-method-bufferconcatlist-totallength [`Buffer.copyBytesFrom()`]: #static-method-buffercopybytesfromview-offset-length [`Buffer.from(array)`]: #static-method-bufferfromarray @@ -5629,10 +5727,11 @@ introducing security vulnerabilities into an application. [`TypedArray.prototype.subarray()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/subarray [`blob.stream()`]: #blobstream [`buf.buffer`]: #bufbuffer +[`buf.byteOffset`]: #bufbyteoffset [`buf.compare()`]: #bufcomparetarget-targetstart-targetend-sourcestart-sourceend [`buf.entries()`]: #bufentries [`buf.fill()`]: #buffillvalue-offset-end-encoding -[`buf.indexOf()`]: #bufindexofvalue-byteoffset-encoding +[`buf.indexOf()`]: #bufindexofvalue-start-end-encoding [`buf.keys()`]: #bufkeys [`buf.length`]: #buflength [`buf.slice()`]: #bufslicestart-end @@ -5643,6 +5742,7 @@ introducing security vulnerabilities into an application. [`buffer.constants.MAX_STRING_LENGTH`]: #bufferconstantsmax_string_length [`buffer.kMaxLength`]: #bufferkmaxlength [`util.inspect()`]: util.md#utilinspectobject-options +[`v8.startupSnapshot.setDeserializeMainFunction()`]: v8.md#v8startupsnapshotsetdeserializemainfunctioncallback-data [`v8::Uint8Array::kMaxLength`]: https://v8.github.io/api/head/classv8_1_1Uint8Array.html#a7677e3d0c9c92e4d40bef7212f5980c6 [base64url]: https://tools.ietf.org/html/rfc4648#section-5 [endianness]: https://en.wikipedia.org/wiki/Endianness diff --git a/doc/api/child_process.md b/doc/api/child_process.md index e90759b16d3f..913542c2dc06 100644 --- a/doc/api/child_process.md +++ b/doc/api/child_process.md @@ -2390,7 +2390,7 @@ or [`child_process.fork()`][]. [`subprocess.stdin`]: #subprocessstdin [`subprocess.stdio`]: #subprocessstdio [`subprocess.stdout`]: #subprocessstdout -[`util.convertProcessSignalToExitCode()`]: util.md#utilconvertprocesssignaltoexitcodesignalcode +[`util.convertProcessSignalToExitCode()`]: util.md#utilconvertprocesssignaltoexitcodesignal [`util.promisify()`]: util.md#utilpromisifyoriginal [synchronous counterparts]: #synchronous-process-creation [v8.serdes]: v8.md#serialization-api diff --git a/doc/api/cli.md b/doc/api/cli.md index 58b9ab0b36b7..7e503bdde4f2 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -208,8 +208,8 @@ starting Node.js. The [`node:ffi`][] module also requires the Example: ```js -const { DynamicLibrary } = require('node:ffi'); -const lib = new DynamicLibrary('mylib.so'); +const { DynamicLibrary, suffix } = require('node:ffi'); +const lib = new DynamicLibrary(`./mylib.${suffix}`); ``` ```console @@ -881,8 +881,9 @@ priority than `--dns-result-order`. added: v6.0.0 --> -Enable FIPS-compliant crypto at startup. (Requires Node.js to be built -against FIPS-compatible OpenSSL.) +Enable [FIPS mode][] at startup. With OpenSSL 3, a configured provider named +`fips` must be available and initialize successfully. With OpenSSL 1.1.1, +Node.js must be built against a FIPS-capable OpenSSL. ### `--enable-source-maps` @@ -1547,25 +1548,6 @@ added: Enable experimental support for the worker inspection with Chrome DevTools. -### `--expose-gc` - - - -> Stability: 1 - Experimental. This flag is inherited from V8 and is subject to -> change upstream. - -This flag will expose the gc extension from V8. - -```js -if (globalThis.gc) { - globalThis.gc(); -} -``` - ### `--force-context-aware` -Force FIPS-compliant crypto on startup. (Cannot be disabled from script code.) -(Same requirements as `--enable-fips`.) +Enable [FIPS mode][] at startup and prevent it from being disabled from script +code. The same OpenSSL requirements as [`--enable-fips`][] apply. ### `--force-node-api-uncaught-exceptions-policy` @@ -2276,9 +2258,11 @@ usually only useful for developers debugging Node.js itself. added: v6.9.0 --> -Load an OpenSSL configuration file on startup. Among other uses, this can be -used to enable FIPS-compliant crypto if Node.js is built -against FIPS-enabled OpenSSL. +Load an OpenSSL configuration file on startup. The file can activate an +OpenSSL 3 FIPS provider or configure a FIPS-capable OpenSSL 1.1.1 build. See +[FIPS mode][]. + +This option takes precedence over the `OPENSSL_CONF` environment variable. ### `--openssl-legacy-provider` @@ -4253,9 +4237,8 @@ environment variable is arbitrary. added: v6.11.0 --> -Load an OpenSSL configuration file on startup. Among other uses, this can be -used to enable FIPS-compliant crypto if Node.js is built with -`./configure --openssl-fips`. +Load an OpenSSL configuration file on startup. The file can be used as part of +a [FIPS mode][] configuration. If the [`--openssl-config`][] command-line option is used, the environment variable is ignored. @@ -4462,6 +4445,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12 [ECMAScript module]: esm.md#modules-ecmascript-modules [EventSource Web API]: https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events [ExperimentalWarning: `vm.measureMemory` is an experimental feature]: vm.md#vmmeasurememoryoptions +[FIPS mode]: crypto.md#fips-mode [File System Permissions]: permissions.md#file-system-permissions [Loading ECMAScript modules using `require()`]: modules.md#loading-ecmascript-modules-using-require [Module resolution and loading]: packages.md#module-resolution-and-loading @@ -4491,6 +4475,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12 [`--cpu-prof-dir`]: #--cpu-prof-dir [`--diagnostic-dir`]: #--diagnostic-dirdirectory [`--disable-sigusr1`]: #--disable-sigusr1 +[`--enable-fips`]: #--enable-fips [`--env-file-if-exists`]: #--env-file-if-existsfile [`--env-file`]: #--env-filefile [`--experimental-sea-config`]: single-executable-applications.md#1-generating-single-executable-preparation-blobs diff --git a/doc/api/crypto.md b/doc/api/crypto.md index 1f2f6acd73e5..413533c63d01 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -4302,11 +4302,8 @@ deprecated: v10.0.0 > Stability: 0 - Deprecated -Property for checking and controlling whether a FIPS compliant crypto provider -is currently in use. Setting to true requires a FIPS build of Node.js. - -This property is deprecated. Please use `crypto.setFips()` and -`crypto.getFips()` instead. +Deprecated property for checking and controlling [FIPS mode][]. Use +[`crypto.getFips()`][] and [`crypto.setFips()`][] instead. ### `crypto.generateKey(type, options, callback)` @@ -4897,9 +4894,14 @@ console.log(aliceSecret === bobSecret); added: v10.0.0 --> -* Returns: {number} `1` if and only if a FIPS compliant crypto provider is - currently in use, `0` otherwise. A future semver-major release may change - the return type of this API to a {boolean}. +* Returns: {number} `1` if FIPS mode is enabled, `0` otherwise. A future + semver-major release may change the return type of this API to a {boolean}. + +With OpenSSL 3, this reports whether the default property query includes +`fips=yes`. It does not establish that a FIPS provider is loaded or validated. +It can return `1` even when a requested cryptographic implementation cannot be +fetched because no loaded provider supplies a match for `fips=yes`. See [FIPS +mode][]. ### `crypto.getHashes()` @@ -5229,6 +5231,10 @@ negative performance implications for some applications; see the -* `password` {string|Buffer|TypedArray|DataView} -* `salt` {string|Buffer|TypedArray|DataView} +* `password` {string|ArrayBuffer|Buffer|TypedArray|DataView} +* `salt` {string|ArrayBuffer|Buffer|TypedArray|DataView} * `iterations` {number} * `keylen` {number} * `digest` {string} @@ -5294,6 +5300,9 @@ An array of supported digest functions can be retrieved using * `privateKey` {Object|string|ArrayBuffer|Buffer|TypedArray|DataView|KeyObject|CryptoKey|URL} - * `oaepHash` {string} The hash function to use for OAEP padding and MGF1. - **Default:** `'sha1'` + * `oaepHash` {string} The hash function to use for OAEP padding and, unless + `mgf1Hash` is set, MGF1. **Default:** `'sha1'` + * `mgf1Hash` {string} The hash function to use for the MGF1 mask generation + function of OAEP padding. If not specified, the value of `oaepHash` is used. + This allows the OAEP digest and the MGF1 digest to differ. * `oaepLabel` {string|ArrayBuffer|Buffer|TypedArray|DataView} The label to use for OAEP padding. If not specified, no label is used. * `padding` {crypto.constants} An optional padding value defined in @@ -5436,6 +5448,9 @@ be passed instead of a public key. -* `bool` {boolean} `true` to enable FIPS mode. +* `bool` {boolean} `true` to enable FIPS mode, `false` to disable it. + +Changes [FIPS mode][]. With OpenSSL 3, this only adds or removes `fips=yes` in +the default property query. It does not install, load, initialize, or validate +a FIPS provider. For a usable FIPS configuration, install the provider and +configure OpenSSL to load it when Node.js starts, as described in [FIPS +mode][]. + +If no loaded provider supplies a requested cryptographic implementation +matching `fips=yes`, the call can still succeed and `crypto.getFips()` can still +return `1`, but fetching that implementation fails. Affected `node:crypto` +operations typically fail with `ERR_OSSL_EVP_UNSUPPORTED`. Operations that do +not require a new fetch, including those using previously fetched +implementations or initialized operation contexts, may still succeed. Call this +method during application initialization, before application code uses other +OpenSSL-backed APIs. -Enables the FIPS compliant crypto provider in a FIPS-enabled Node.js build. -Throws an error if FIPS mode is not available. +This method only affects subsequent algorithm fetches. Node.js initializes some +OpenSSL state before application code runs. When the property query must be +active from process startup, set `default_properties = fips=yes` in the OpenSSL +configuration or use [`--enable-fips`][] or [`--force-fips`][]. The command-line +flags additionally require a configured provider named `fips` to initialize and +pass its self-test; Node.js fails to start otherwise. + +Throws an error if OpenSSL cannot change the state. FIPS mode cannot be +disabled when Node.js was started with `--force-fips`. With OpenSSL 1.1.1, +enabling FIPS mode requires a FIPS-capable OpenSSL build. ### `crypto.sign(algorithm, data, key[, callback])` @@ -6605,83 +6652,120 @@ console.log(receivedPlaintext); ### FIPS mode -When using OpenSSL 3, Node.js supports FIPS 140-2 when used with an appropriate -OpenSSL 3 provider, such as the [FIPS provider from OpenSSL 3][] which can be -installed by following the instructions in [OpenSSL's FIPS README file][]. +Node.js exposes the FIPS support provided by the linked OpenSSL library. Node.js +is not itself FIPS validated. Validation belongs to a specific OpenSSL module or +provider and only applies when it is deployed according to its security policy. +Vendor-provided Node.js or OpenSSL builds can require a different configuration; +follow the vendor's documentation for those builds. + +With OpenSSL 1.1.1, Node.js must be built against a FIPS-capable OpenSSL library. -For FIPS support in Node.js you will need: +With OpenSSL 3, FIPS support uses the provider model described in the +[OpenSSL FIPS module guide][]. Using FIPS-approved implementations requires: * A correctly installed OpenSSL 3 FIPS provider. * An OpenSSL 3 [FIPS module configuration file][]. -* An OpenSSL 3 configuration file that references the FIPS module - configuration file. +* The FIPS provider to be loaded into the OpenSSL library context used by + Node.js, normally by activating it in an OpenSSL configuration file when + Node.js starts. +* The default property query to include `fips=yes` when cryptographic + implementations are fetched. This can be set from process startup by the + OpenSSL configuration, [`--enable-fips`][], or [`--force-fips`][], or for + subsequent fetches by `crypto.setFips(true)`. -Node.js will need to be configured with an OpenSSL configuration file that -points to the FIPS provider. An example configuration file looks like this: +An example OpenSSL 3 configuration file looks like this: ```text nodejs_conf = nodejs_init +config_diagnostics = 1 .include //fipsmodule.cnf [nodejs_init] providers = provider_sect +alg_section = algorithm_sect [provider_sect] -default = default_sect # The fips section name should match the section name inside the # included fipsmodule.cnf. fips = fips_sect +base = base_sect -[default_sect] +[base_sect] activate = 1 -``` - -where `fipsmodule.cnf` is the FIPS module configuration file generated from the -FIPS provider installation step: -```bash -openssl fipsinstall +[algorithm_sect] +default_properties = fips=yes ``` -Set the `OPENSSL_CONF` environment variable to point to -your configuration file and `OPENSSL_MODULES` to the location of the FIPS -provider dynamic library. e.g. +The `fipsmodule.cnf` file is generated as part of the FIPS provider installation +and contains module integrity and self-test information. The exact command and +arguments are installation-specific; see [OpenSSL FIPS configuration][] and the +[OpenSSL FIPS module guide][]. The installation uses `openssl fipsinstall`. + +The example activates the provider and enables the `fips=yes` property query +when Node.js starts. To activate the provider at startup but enable the property +query later with `crypto.setFips(true)`, omit `alg_section = algorithm_sect` and +the `[algorithm_sect]` block. The provider must still be loaded; when using this +startup configuration, keep its activation enabled. `crypto.setFips(true)` +should be called before application code uses other OpenSSL-backed APIs. It is +not equivalent to enabling the property query from process startup because +Node.js initializes some OpenSSL state before application code runs. Use the +example as written, [`--enable-fips`][], or [`--force-fips`][] when the property +query must be active from process startup. + +`config_diagnostics` causes configuration errors to prevent startup instead of +being ignored. The `base` provider supplies non-cryptographic supporting +algorithms, such as encoders and decoders, that are commonly needed alongside +the FIPS provider. `default_properties = fips=yes` restricts OpenSSL's default +algorithm selection to implementations that match `fips=yes`. + +Set `OPENSSL_CONF` to the OpenSSL configuration file. For a dynamically loaded +provider, `OPENSSL_MODULES` can set the directory containing the provider module. +For example: ```bash export OPENSSL_CONF=//nodejs.cnf export OPENSSL_MODULES=//ossl-modules ``` -FIPS mode can then be enabled in Node.js either by: - -* Starting Node.js with `--enable-fips` or `--force-fips` command line flags. -* Programmatically calling `crypto.setFips(true)`. - -Optionally FIPS mode can be enabled in Node.js via the OpenSSL configuration -file. e.g. - -```text -nodejs_conf = nodejs_init - -.include //fipsmodule.cnf - -[nodejs_init] -providers = provider_sect -alg_section = algorithm_sect - -[provider_sect] -default = default_sect -# The fips section name should match the section name inside the -# included fipsmodule.cnf. -fips = fips_sect - -[default_sect] -activate = 1 - -[algorithm_sect] -default_properties = fips=yes -``` +The [`--openssl-config`][] command-line option selects the configuration file and +takes precedence over `OPENSSL_CONF`. If neither is set, OpenSSL's default +configuration file is used. + +By default, Node.js reads the `nodejs_conf` section instead of OpenSSL's usual +`openssl_conf` section. Use [`--openssl-shared-config`][] to read `openssl_conf`, +or build Node.js with `./configure --openssl-conf-name=` to change the +default section name. + +On OpenSSL 3, the configuration above enables the `fips=yes` property query at +startup. The following controls are also available: + +* [`--enable-fips`][] and [`--force-fips`][] enable the property query and + additionally require the configured provider named `fips` to initialize and + pass its self-test. Node.js exits if that check fails. `--force-fips` also + prevents FIPS mode from being disabled from script code. +* [`crypto.setFips()`][] changes the FIPS/property-query state. On OpenSSL 3, it + does not install, load, initialize, or validate a provider. Implementations + fetched before the call are not changed. +* [`crypto.getFips()`][] reports the FIPS/property-query state. On OpenSSL 3, a + return value of `1` does not prove that a FIPS provider is loaded or validated. + +With OpenSSL 1.1.1, these controls use the library's FIPS mode support and +require a FIPS-capable OpenSSL build. + +Only algorithms available under the active FIPS settings can be used. With +OpenSSL 3, if no loaded provider supplies a requested cryptographic +implementation matching `fips=yes`, fetching it fails, typically with +`ERR_OSSL_EVP_UNSUPPORTED`. The same error can occur for algorithms that +Node.js supports when FIPS mode is disabled but that are unavailable under the +active FIPS settings. + +OpenSSL documents that the same FIPS provider cannot be used by multiple copies +of `libcrypto` in one process. This can affect native addons that load another +copy of `libcrypto`; OpenSSL's documented workaround is to use a separate copy +of the provider for each `libcrypto` instance. See [OpenSSL FIPS provider +limitations][]. ## Crypto constants @@ -6964,15 +7048,17 @@ See the [list of SSL OP Flags][] for details. [CVE-2021-44532]: https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-44532 [Caveats]: #support-for-weak-or-compromised-algorithms [Crypto constants]: #crypto-constants -[FIPS module configuration file]: https://www.openssl.org/docs/man3.0/man5/fips_config.html -[FIPS provider from OpenSSL 3]: https://www.openssl.org/docs/man3.0/man7/crypto.html#FIPS-provider +[FIPS mode]: #fips-mode +[FIPS module configuration file]: https://docs.openssl.org/3.0/man5/fips_config/ [HTML 5.2]: https://www.w3.org/TR/html52/changes.html#features-removed [JWK]: https://tools.ietf.org/html/rfc7517 [Key usages]: webcrypto.md#cryptokeyusages [NIST SP 800-131A]: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-131Ar2.pdf [NIST SP 800-132]: https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-132.pdf [NIST SP 800-38D]: https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf -[OpenSSL's FIPS README file]: https://github.com/openssl/openssl/blob/openssl-3.0/README-FIPS.md +[OpenSSL FIPS configuration]: https://docs.openssl.org/3.0/man5/fips_config/ +[OpenSSL FIPS module guide]: https://docs.openssl.org/master/man7/fips_module/ +[OpenSSL FIPS provider limitations]: https://docs.openssl.org/3.6/man7/OSSL_PROVIDER-FIPS/ [OpenSSL's SPKAC implementation]: https://www.openssl.org/docs/man3.0/man1/openssl-spkac.html [Permission Model]: permissions.md#permission-model [RFC 1421]: https://www.rfc-editor.org/rfc/rfc1421.txt @@ -6989,6 +7075,10 @@ See the [list of SSL OP Flags][] for details. [RFC 9562]: https://www.rfc-editor.org/rfc/rfc9562.txt [Web Crypto API documentation]: webcrypto.md [`--allow-openssl-store`]: cli.md#--allow-openssl-store +[`--enable-fips`]: cli.md#--enable-fips +[`--force-fips`]: cli.md#--force-fips +[`--openssl-config`]: cli.md#--openssl-configfile +[`--openssl-shared-config`]: cli.md#--openssl-shared-config [`BN_is_prime_ex`]: https://www.openssl.org/docs/man1.1.1/man3/BN_is_prime_ex.html [`Buffer`]: buffer.md [`DH_generate_key()`]: https://www.openssl.org/docs/man3.0/man3/DH_generate_key.html @@ -7015,6 +7105,7 @@ See the [list of SSL OP Flags][] for details. [`crypto.generateKeyPair()`]: #cryptogeneratekeypairtype-options-callback [`crypto.getCurves()`]: #cryptogetcurves [`crypto.getDiffieHellman()`]: #cryptogetdiffiehellmangroupname +[`crypto.getFips()`]: #cryptogetfips [`crypto.getHashes()`]: #cryptogethashes [`crypto.hash()`]: #cryptohashalgorithm-data-options [`crypto.privateDecrypt()`]: #cryptoprivatedecryptprivatekey-buffer @@ -7023,6 +7114,7 @@ See the [list of SSL OP Flags][] for details. [`crypto.publicEncrypt()`]: #cryptopublicencryptkey-buffer [`crypto.randomBytes()`]: #cryptorandombytessize-callback [`crypto.randomFill()`]: #cryptorandomfillbuffer-offset-size-callback +[`crypto.setFips()`]: #cryptosetfipsbool [`crypto.sign()`]: #cryptosignalgorithm-data-key-callback [`crypto.verify()`]: #cryptoverifyalgorithm-data-key-signature-callback [`crypto.webcrypto.getRandomValues()`]: webcrypto.md#cryptogetrandomvaluestypedarray diff --git a/doc/api/deprecations.md b/doc/api/deprecations.md index 334b94f6bb8f..9b18c4ed486f 100644 --- a/doc/api/deprecations.md +++ b/doc/api/deprecations.md @@ -4624,7 +4624,7 @@ will throw an error in a future version. [`--pending-deprecation`]: cli.md#--pending-deprecation [`--throw-deprecation`]: cli.md#--throw-deprecation [`--unhandled-rejections`]: cli.md#--unhandled-rejectionsmode -[`Buffer.allocUnsafeSlow(size)`]: buffer.md#static-method-bufferallocunsafeslowsize +[`Buffer.allocUnsafeSlow(size)`]: buffer.md#static-method-bufferallocunsafeslowsize-alignment [`Buffer.from(array)`]: buffer.md#static-method-bufferfromarray [`Buffer.from(buffer)`]: buffer.md#static-method-bufferfrombuffer [`Buffer.isBuffer()`]: buffer.md#static-method-bufferisbufferobj @@ -4770,7 +4770,7 @@ will throw an error in a future version. [`writable.writableLength`]: stream.md#writablewritablelength [`zlib.bytesWritten`]: zlib.md#zlibbyteswritten [alloc]: buffer.md#static-method-bufferallocsize-fill-encoding -[alloc_unsafe_size]: buffer.md#static-method-bufferallocunsafesize +[alloc_unsafe_size]: buffer.md#static-method-bufferallocunsafesize-alignment [caveats of asynchronous customization hooks]: module.md#caveats-of-asynchronous-customization-hooks [from_arraybuffer]: buffer.md#static-method-bufferfromarraybuffer-byteoffset-length [from_string_encoding]: buffer.md#static-method-bufferfromstring-encoding diff --git a/doc/api/diagnostics_channel.md b/doc/api/diagnostics_channel.md index f98d90ab2a19..e3a6a5e3fa98 100644 --- a/doc/api/diagnostics_channel.md +++ b/doc/api/diagnostics_channel.md @@ -237,9 +237,13 @@ diagnostics_channel.unsubscribe('my-channel', onMessage); added: - v19.9.0 - v18.19.0 +changes: + - version: REPLACEME + pr-url: https://github.com/nodejs/node/pull/64525 + description: Marked as stable. --> -> Stability: 1 - Experimental +> Stability: 2 - Stable * `nameOrChannels` {string|TracingChannel} Channel name or object containing all the [TracingChannel Channels][] @@ -744,9 +748,13 @@ The scope must be used with the `using` syntax to ensure proper disposal. added: - v19.9.0 - v18.19.0 +changes: + - version: REPLACEME + pr-url: https://github.com/nodejs/node/pull/64525 + description: Marked as stable. --> -> Stability: 1 - Experimental +> Stability: 2 - Stable The class `TracingChannel` is a collection of [TracingChannel Channels][] which together express a single traceable action. It is used to formalize and diff --git a/doc/api/ffi.md b/doc/api/ffi.md index 9df2a9789d98..158aadffa3e3 100644 --- a/doc/api/ffi.md +++ b/doc/api/ffi.md @@ -363,7 +363,8 @@ The returned function has a `.pointer` property containing the native function address as a `bigint`. If the same symbol has already been resolved, requesting it again with a -different signature throws. +different signature throws. Requesting it again with the same signature returns +the same function, as does reading it from [`library.functions`][]. ```cjs const { DynamicLibrary, suffix } = require('node:ffi'); @@ -714,7 +715,7 @@ available storage. This function does not allocate memory on its own. added: v26.1.0 --> -* `source` {Buffer|ArrayBuffer|ArrayBufferView} +* `source` {Buffer|ArrayBuffer|SharedArrayBuffer|ArrayBufferView} * Returns: {bigint} Returns the raw memory address of JavaScript-managed byte storage. @@ -766,5 +767,6 @@ and keep callback and pointer lifetimes explicit on the native side. [Permission Model]: permissions.md#permission-model [`--allow-ffi`]: cli.md#--allow-ffi [`ffi.toBuffer(pointer, length, copy)`]: #ffitobufferpointer-length-copy +[`library.functions`]: #libraryfunctions [`using`]: https://tc39.es/proposal-explicit-resource-management/#sec-using-declarations [type names]: #type-names diff --git a/doc/api/fs.md b/doc/api/fs.md index dea508929fe2..dd59bce3287e 100644 --- a/doc/api/fs.md +++ b/doc/api/fs.md @@ -323,6 +323,9 @@ fd.createReadStream({ start: 90, end: 99 }); + +* `addresses` {string\[]|net.SocketAddress\[]} An array of IPv4 or IPv6 + addresses. +* `type` {string} Either `'ipv4'` or `'ipv6'`. **Default:** `'ipv4'`. + +Adds multiple address rules to the block list in a single operation. +This is more efficient than calling `blockList.addAddress()` repeatedly +when adding a large number of individual addresses, as the addresses +are inserted under a single internal lock acquisition. + +### `blockList.addCIDR(cidr)` + + + +* `cidr` {string} An IPv4 or IPv6 subnet in CIDR notation (e.g. + `'10.0.0.0/8'` or `'2001:db8::/32'`). + +Adds a subnet rule using CIDR notation. The address family is automatically +detected from the address (IPv6 if the address contains `':'`, IPv4 +otherwise). This is equivalent to calling `blockList.addSubnet()` with +the parsed network address, prefix length, and family. + +### `blockList.addCIDRs(cidrs)` + + + +* `cidrs` {string\[]} An array of IPv4 or IPv6 subnets in CIDR notation. + +Adds multiple subnet rules using CIDR notation in a single call. The address +family for each entry is automatically detected. This is equivalent to +calling `blockList.addCIDR()` for each element of the array. + ### `blockList.addRange(start, end[, type])` -* Type: {string\[]} - -The list of rules added to the blocklist. - -### `BlockList.isBlockList(value)` - - - -* `value` {any} Any JS value -* Returns `true` if the `value` is a `net.BlockList`. +Clears all rules from the `BlockList`. ### `blockList.fromJSON(value)` @@ -205,6 +231,130 @@ blockList.fromJSON(JSON.stringify(data)); * `value` Blocklist.rules +### `BlockList.isBlockList(value)` + + + +* `value` {any} Any JS value +* Returns `true` if the `value` is a `net.BlockList`. + +### `BlockList.PRIVATE_RANGES` + + + +* Type: {string\[]} + +A frozen array of CIDR strings representing private, loopback, and link-local +IP address ranges. This can be passed to `blockList.addCIDRs()` to quickly +populate a blocklist with all non-routable address ranges. + +The included ranges are: + +* `10.0.0.0/8` — RFC 1918 private IPv4 +* `172.16.0.0/12` — RFC 1918 private IPv4 +* `192.168.0.0/16` — RFC 1918 private IPv4 +* `127.0.0.0/8` — IPv4 loopback +* `::1/128` — IPv6 loopback +* `169.254.0.0/16` — IPv4 link-local +* `fe80::/10` — IPv6 link-local +* `fc00::/7` — IPv6 unique local (ULA) + +```js +const blockList = new net.BlockList(); +blockList.addCIDRs(net.BlockList.PRIVATE_RANGES); + +console.log(blockList.check('10.0.0.1')); // Prints: true +console.log(blockList.check('127.0.0.1')); // Prints: true +console.log(blockList.check('8.8.8.8')); // Prints: false +``` + +### `blockList.removeAddress(address[, type])` + + + +* `address` {string|net.SocketAddress} An IPv4 or IPv6 address. +* `type` {string} Either `'ipv4'` or `'ipv6'`. **Default:** `'ipv4'`. + +Removes a rule that was previously added with `blockList.addAddress()`. The +address must match exactly the value used when the rule was added. If the +specified address does not exist, this is a no-op. + +### `blockList.removeCIDR(cidr)` + + + +* `cidr` {string} An IPv4 or IPv6 subnet in CIDR notation (e.g. + `'10.0.0.0/8'` or `'2001:db8::/32'`). + +Removes a subnet rule using CIDR notation. The address family is automatically +detected from the address. This is equivalent to calling +`blockList.removeSubnet()` with the parsed network address, prefix length, +and family. If the specified subnet does not exist, this is a no-op. + +### `blockList.removeRange(start, end[, type])` + + + +* `start` {string|net.SocketAddress} The starting IPv4 or IPv6 address in the + range. +* `end` {string|net.SocketAddress} The ending IPv4 or IPv6 address in the range. +* `type` {string} Either `'ipv4'` or `'ipv6'`. **Default:** `'ipv4'`. + +Removes a rule that was previously added with `blockList.addRange()`. The `start` +and `end` addresses must match exactly the values used when the rule was added. +If the specified range does not exist, this is a no-op. + +### `blockList.removeSubnet(net, prefix[, type])` + + + +* `net` {string|net.SocketAddress} The network IPv4 or IPv6 address. +* `prefix` {number} The number of CIDR prefix bits. For IPv4, this + must be a value between `0` and `32`. For IPv6, this must be between + `0` and `128`. +* `type` {string} Either `'ipv4'` or `'ipv6'`. **Default:** `'ipv4'`. + +Removes a rule that was previously added with `blockList.addSubnet()`. The +network address and prefix must match exactly the values used when the rule was +added. If the specified subnet does not exist, this is a no-op. + +### `blockList.rules` + + + +* Type: {string\[]} + +The list of rules added to the blocklist. + +### `blockList.size` + + + +* Type: {number} + +The number of rules in the blocklist. This is equivalent to +`blockList.rules.length` but does not allocate the rules array. + ### `blockList.toJSON()` > Stability: 1.2 - Release candidate diff --git a/doc/api/perf_hooks.md b/doc/api/perf_hooks.md index 78a3b6210326..544c25e9fdc9 100644 --- a/doc/api/perf_hooks.md +++ b/doc/api/perf_hooks.md @@ -1866,6 +1866,45 @@ added: The number of samples recorded by the histogram. +### `histogram.ccdf(value)` + + + +* `value` {number} The value to query. +* Returns: {number} A probability between 0.0 and 1.0. + +Returns the complementary cumulative distribution function (CCDF) value +for the given value, representing the probability that a recorded value +will exceed `value`. Equivalent to `1 - histogram.cdf(value)`. + +### `histogram.cdf(value)` + + + +* `value` {number} The value to query. +* Returns: {number} A probability between 0.0 and 1.0. + +Returns the cumulative distribution function (CDF) value for the given +value, representing the probability that a recorded value will be less +than or equal to `value`. This is the inverse operation of +`histogram.percentile()`. + +### `histogram.countAt(value)` + + + +* `value` {number} The value to query. +* Returns: {number} + +Returns the number of recorded values that fall within the equivalent +value range of the given value. + ### `histogram.exceeds` + +* `other` {Histogram} The histogram to compare against. +* Returns: {number} The KS D-statistic, between 0.0 and 1.0. + +Computes the Kolmogorov-Smirnov test statistic comparing this histogram's +distribution to `other`. A value of 0 indicates identical distributions; +values close to 1 indicate completely disjoint distributions. Useful for +detecting performance regressions by comparing before/after histograms. + +### `histogram.kurtosis` + + + +* Type: {number} + +The excess kurtosis of the recorded values. Measures the heaviness of the +distribution's tails relative to a normal distribution. Positive values +indicate heavier tails (more extreme outliers); negative values indicate +lighter tails. + +### `histogram.linearBuckets(stepSize)` + + + +* `stepSize` {number} The width of each linear bucket. +* Returns: {Map} A map of bucket boundary values to counts. + +Returns the histogram data rebucketed into linearly-spaced intervals +of `stepSize`. Useful for visualization and export. + +### `histogram.logBuckets(firstBucket, base)` + + + +* `firstBucket` {number} The value of the first bucket boundary. +* `base` {number} The logarithmic base for bucket width growth. Must be > 1. +* Returns: {Map} A map of bucket boundary values to counts. + +Returns the histogram data rebucketed into logarithmically-spaced +intervals, where each bucket's width is multiplied by `base`. +Useful for visualization and export. + ### `histogram.max` + +* `percentiles` {number\[]} An array of percentile values in the range (0, 100]. +* Returns: {Map} A map of percentile values to their corresponding histogram + values. + +Returns the values at the specified percentiles, computed in a single +efficient pass over the histogram data. More efficient than calling +`histogram.percentile()` multiple times. + ### `histogram.reset()` + +* Type: {number} + +The skewness of the recorded values. Measures the asymmetry of the +distribution. A positive value indicates a right-skewed distribution +(longer right tail, common for latency data); a negative value +indicates a left-skewed distribution. + ### `histogram.stddev` + +* `val` {number|bigint} The value to record. +* `expectedInterval` {number|bigint} The expected recording interval. + +Records a value with coordinated omission correction. When a system stall +prevents timely recording, this method backfills intermediate values at +`expectedInterval` steps between the previously recorded value and `val`. +This compensates for measurement gaps that would otherwise underrepresent +latency. + +### `histogram.subtract(other)` + + + +* `other` {RecordableHistogram} + +Subtracts the values of `other` from this histogram. Both histograms should +have compatible configurations. Bucket counts that would become negative +are clamped to zero. + +## Histogram analysis examples + +The `Histogram` class provides statistical analysis methods useful for +performance monitoring, SLO enforcement, and regression detection. + +### Distribution shape analysis + +```js +const { createHistogram } = require('node:perf_hooks'); + +const h = createHistogram(); + +// Simulate a right-skewed latency distribution +for (let i = 0; i < 1000; i++) { + h.record(Math.ceil(Math.random() * 100)); +} +// Add some outliers +for (let i = 0; i < 10; i++) { + h.record(500 + Math.ceil(Math.random() * 500)); +} + +console.log('Skewness:', h.skewness.toFixed(4)); // Positive = right-skewed +console.log('Kurtosis:', h.kurtosis.toFixed(4)); // Positive = heavy tails +``` + +### SLO monitoring with CDF + +```js +const { createHistogram } = require('node:perf_hooks'); + +const latency = createHistogram(); + +// Record request latencies (in nanoseconds)... + +// "What fraction of requests complete within 100ms?" +const withinSLO = latency.cdf(100_000_000); +console.log(`${(withinSLO * 100).toFixed(1)}% of requests within SLO`); + +// "What fraction of requests exceed 500ms?" +const violating = latency.ccdf(500_000_000); +console.log(`${(violating * 100).toFixed(1)}% of requests violating SLO`); +``` + +### Regression detection with KS test + +```js +const { createHistogram } = require('node:perf_hooks'); + +const baseline = createHistogram(); +const current = createHistogram(); + +// Record baseline and current latencies... + +// D-statistic: 0 = identical, 1 = completely different +const d = baseline.ksTest(current); +if (d > 0.1) { + console.log(`Possible regression detected (D=${d.toFixed(4)})`); +} +``` + +### Batch percentile queries + +```js +const { createHistogram } = require('node:perf_hooks'); + +const h = createHistogram(); +// Record values... + +// Efficiently query common monitoring percentiles in one pass +const p = h.percentilesAt([50, 75, 90, 95, 99, 99.9]); +console.log('p50:', p.get(50)); +console.log('p99:', p.get(99)); +``` + +### Snapshot diffing with subtract + +```js +const { createHistogram } = require('node:perf_hooks'); + +const total = createHistogram(); +const snapshot = createHistogram(); + +// Record values into total... +// Periodically snapshot for "last interval" analysis: +snapshot.add(total); + +// Later, take a new snapshot and diff: +const newSnapshot = createHistogram(); +newSnapshot.add(total); +newSnapshot.subtract(snapshot); +// newSnapshot now contains only the values recorded since the last snapshot +console.log('Recent p99:', newSnapshot.percentile(99)); +``` + ## Examples ### Measuring the duration of async operations diff --git a/doc/api/permissions.md b/doc/api/permissions.md index c387e899cf1a..0b46f42d982a 100644 --- a/doc/api/permissions.md +++ b/doc/api/permissions.md @@ -138,7 +138,7 @@ const config = fs.readFileSync('/etc/myapp/config.json', 'utf8'); // Drop read access to /etc/myapp after initialization process.permission.drop('fs.read', '/etc/myapp'); -// This will now throw ERR_ACCESS_DENIED +// This will now return false process.permission.has('fs.read', '/etc/myapp/config.json'); // false // Drop child process permission entirely @@ -219,7 +219,7 @@ $ node --permission index.js * `index.js` will be included in the allowed file system read list ```console -$ node -r /path/to/custom-require.js --permission index.js. +$ node -r /path/to/custom-require.js --permission index.js ``` * `/path/to/custom-require.js` will be included in the allowed file system read diff --git a/doc/api/quic.md b/doc/api/quic.md index 2a657cb6834e..b23f27984fc0 100644 --- a/doc/api/quic.md +++ b/doc/api/quic.md @@ -1921,9 +1921,14 @@ True if `stream.destroy()` has been called. ### Aborting a stream -A QuicStream can be aborted in three ways, each producing different +A QuicStream can be aborted in several ways, each producing different wire-frame side effects: +* [`stream.stopSending()`][] — Aborts only the readable side. Sends + `STOP_SENDING` to the peer. The writable side is unaffected. +* [`stream.resetStream()`][] — Aborts only the writable side. Sends + `RESET_STREAM` to the peer. Unlike [`writer.fail(reason)`][], the wire + code is given directly rather than derived from an error. * [`writer.fail(reason)`][] — Aborts only the writable side. Sends `RESET_STREAM` to the peer. The readable side is unaffected; any data already buffered for read remains available. @@ -1941,6 +1946,46 @@ the wire code for both `writer.fail()` and `stream.destroy()`. Otherwise the implementation falls back to the negotiated application protocol's "internal error" code (see [`QuicError`][]). +[`stream.stopSending()`][] and [`stream.resetStream()`][] do +not perform this derivation: they send `code` as given. + +### `stream.resetStream([code])` + + + +* `code` {number|bigint} The application error code to send to the peer. + **Default:** `0n`. + +Tells the peer that this end will not send any more data on this stream, +sending a `RESET_STREAM` frame carrying `code`. The readable side is left +open, so data already sent by the peer remains available to read. + +Any data still queued for sending is discarded. A reset stream is never +acknowledged by the peer, so the outbound queue can no longer drain. + +No acknowledgement of this action is provided. The call does nothing if the +stream has been destroyed, if it has already been reset, or if it is a +remote-initiated unidirectional stream, which has no writable side to abort. + +### `stream.stopSending([code])` + + + +* `code` {number|bigint} The application error code to send to the peer. + **Default:** `0n`. + +Asks the peer to stop sending data on this stream, sending a `STOP_SENDING` +frame carrying `code`. The writable side is left open, so this end can +still send data. + +No acknowledgement of this action is provided. The call does nothing if the +stream has been destroyed, or if it is a locally-initiated unidirectional +stream, which has no readable side to abort. + ### `stream.early` Closes the database connection. An exception is thrown if the database is not -open. This method is a wrapper around [`sqlite3_close_v2()`][]. +open. An [`ERR_INVALID_STATE`][] error is thrown if the method is called while +a statement is executing, such as inside a user-defined function, an aggregate +function, or an authorizer callback. This method is a wrapper around +[`sqlite3_close_v2()`][]. ### `database.loadExtension(path[, entryPoint])` @@ -461,7 +478,8 @@ db.setAuthorizer((actionCode) => { }); // This will work -db.prepare('SELECT 1').get(); +using query = db.prepare('SELECT 1'); +query.get(); // This will throw an error due to authorization denial try { @@ -484,7 +502,8 @@ db.setAuthorizer((actionCode) => { }); // This will work -db.prepare('SELECT 1').get(); +using query = db.prepare('SELECT 1'); +query.get(); // This will throw an error due to authorization denial try { @@ -605,8 +624,10 @@ added: v26.1.0 Loads a serialized database into this connection, replacing the current database. The deserialized database is writable. Existing prepared statements are finalized before deserialization is attempted, even if the operation -subsequently fails. This method is a wrapper around -[`sqlite3_deserialize()`][]. +subsequently fails. An [`ERR_INVALID_STATE`][] error is thrown if the method is +called while a database callback is on the stack, for example a user-defined +function, an aggregate function, an authorizer, or a changeset filter or conflict +handler. This method is a wrapper around [`sqlite3_deserialize()`][]. ```mjs import { DatabaseSync } from 'node:sqlite'; @@ -619,7 +640,8 @@ original.close(); const clone = new DatabaseSync(':memory:'); clone.deserialize(buffer); -console.log(clone.prepare('SELECT value FROM t').get()); +using query = clone.prepare('SELECT value FROM t'); +console.log(query.get()); // Prints: { value: 'hello' } ``` @@ -634,7 +656,8 @@ original.close(); const clone = new DatabaseSync(':memory:'); clone.deserialize(buffer); -console.log(clone.prepare('SELECT value FROM t').get()); +using query = clone.prepare('SELECT value FROM t'); +console.log(query.get()); // Prints: { value: 'hello' } ``` @@ -642,6 +665,10 @@ console.log(clone.prepare('SELECT value FROM t').get()); * `sql` {string} A SQL string to compile to a prepared statement. @@ -691,7 +718,8 @@ sqlTagStore.get`SELECT ${value}`; is equivalent to: ```js -db.prepare('SELECT ?').get(value); +using statement = db.prepare('SELECT ?'); +statement.get(value); ``` However, in the first example, the tag store will cache the underlying prepared @@ -811,6 +839,7 @@ added: --> * `changeset` {Uint8Array} A binary changeset or patchset. + * `options` {Object} The configuration options for how the changes will be applied. * `filter` {Function} for each table affected by at least one change in the changeset, the `filter` callback is invoked with the @@ -839,6 +868,7 @@ added: applying the changeset is aborted and the database is rolled back. **Default**: A function that returns `SQLITE_CHANGESET_ABORT`. + * Returns: {boolean} Whether the changeset was applied successfully without being aborted. An exception is thrown if the database is not @@ -855,7 +885,7 @@ targetDb.exec('CREATE TABLE data(key INTEGER PRIMARY KEY, value TEXT)'); const session = sourceDb.createSession(); -const insert = sourceDb.prepare('INSERT INTO data (key, value) VALUES (?, ?)'); +using insert = sourceDb.prepare('INSERT INTO data (key, value) VALUES (?, ?)'); insert.run(1, 'hello'); insert.run(2, 'world'); @@ -875,7 +905,7 @@ targetDb.exec('CREATE TABLE data(key INTEGER PRIMARY KEY, value TEXT)'); const session = sourceDb.createSession(); -const insert = sourceDb.prepare('INSERT INTO data (key, value) VALUES (?, ?)'); +using insert = sourceDb.prepare('INSERT INTO data (key, value) VALUES (?, ?)'); insert.run(1, 'hello'); insert.run(2, 'world'); @@ -964,11 +994,61 @@ times with different bound values. Parameters also offer protection against [SQL injection][] attacks. For these reasons, prepared statements are preferred over hand-crafted SQL strings when handling user input. +### Binding parameters + +The `all()`, `get()`, `iterate()`, and `run()` methods bind their arguments to +the parameters of the prepared statement before executing it. Parameters are +either anonymous or named. + +Anonymous parameters are written as `?` in SQL and are bound in order from the +arguments passed to the method. The `?NNN` form assigns SQLite parameter index +`NNN` to a placeholder. Avoid mixing numbered and named parameters because they +share parameter indexes. + +```js +db.prepare('SELECT ? AS a, ? AS b').get('x', 42); +// { a: 'x', b: 42 } +db.prepare('SELECT ?2 AS a, ?1 AS b').get('first', 'second'); +// { a: 'second', b: 'first' } +``` + +Named parameters begin with one of the prefix characters `$`, `:`, or `@` in +SQL. They are bound from an object passed as the first argument. Repeating a +name in the SQL binds the same value to every occurrence. + +```js +db.prepare('SELECT $a AS a, $b AS b').get({ $a: 1, $b: 2 }); +// { a: 1, b: 2 } +db.prepare('SELECT :a AS a').get({ ':a': 1 }); +// { a: 1 } +db.prepare('SELECT @a AS a').get({ '@a': 1 }); +// { a: 1 } +db.prepare('SELECT $k AS a, $k AS b').get({ k: 7 }); +// { a: 7, b: 7 } +``` + +The last example omits the prefix character from the object key. Bare names are +allowed by default; see [`statement.setAllowBareNamedParameters()`][] for their +caveats. + +Binding a key that does not name a parameter of the statement throws an +`ERR_INVALID_STATE` error unless unknown named parameters are ignored. See +[`statement.setAllowUnknownNamedParameters()`][]. + +See [Type conversion between JavaScript and SQLite][] for the values that can be +bound. Binding any other value throws an `ERR_INVALID_ARG_TYPE` error. + ### `statement.all([namedParameters][, ...anonymousParameters])` + +Finalizes the prepared statement. An exception is thrown if the statement is +already finalized. This method is a wrapper around [`sqlite3_finalize()`][]. ### `statement.columns()` @@ -999,7 +1089,6 @@ added: * Returns: {Array} An array of objects. Each object corresponds to a column in the prepared statement, and contains the following properties: - * `column` {string|null} The unaliased name of the column in the origin table, or `null` if the column is the result of an expression or subquery. This property is the result of [`sqlite3_column_origin_name()`][]. @@ -1037,6 +1126,12 @@ execution of this prepared statement. This property is a wrapper around + +Finalizes the prepared statement. If the prepared statement is already +finalized, then this is a no-op. + ## Class: `SQLTagStore` * `stringElements` {string\[]} Template literal elements containing the SQL query. -* `...boundParameters` {null|number|bigint|string|Buffer|TypedArray|DataView} +* `...boundParameters` {null|number|bigint|boolean|string|Buffer|TypedArray|DataView|ArrayBuffer|SharedArrayBuffer} Parameter values to be bound to placeholders in the template string. * Returns: {Array} An array of objects representing the rows returned by the query. @@ -1235,11 +1362,18 @@ called directly. * `stringElements` {string\[]} Template literal elements containing the SQL query. -* `...boundParameters` {null|number|bigint|string|Buffer|TypedArray|DataView} +* `...boundParameters` {null|number|bigint|boolean|string|Buffer|TypedArray|DataView|ArrayBuffer|SharedArrayBuffer} Parameter values to be bound to placeholders in the template string. * Returns: {Object | undefined} An object representing the first row returned by the query, or `undefined` if no rows are returned. @@ -1253,11 +1387,18 @@ called directly. * `stringElements` {string\[]} Template literal elements containing the SQL query. -* `...boundParameters` {null|number|bigint|string|Buffer|TypedArray|DataView} +* `...boundParameters` {null|number|bigint|boolean|string|Buffer|TypedArray|DataView|ArrayBuffer|SharedArrayBuffer} Parameter values to be bound to placeholders in the template string. * Returns: {Iterator} An iterator that yields objects representing the rows returned by the query. @@ -1270,11 +1411,18 @@ called directly. * `stringElements` {string\[]} Template literal elements containing the SQL query. -* `...boundParameters` {null|number|bigint|string|Buffer|TypedArray|DataView} +* `...boundParameters` {null|number|bigint|boolean|string|Buffer|TypedArray|DataView|ArrayBuffer|SharedArrayBuffer} Parameter values to be bound to placeholders in the template string. * Returns: {Object} An object containing information about the execution, including `changes` and `lastInsertRowid`. @@ -1348,7 +1496,7 @@ changes: database that have been added with [`ATTACH DATABASE`][] **Default:** `'main'`. * `target` {string} Name of the target database. This can be `'main'` (the default primary database) or any other database that have been added with [`ATTACH DATABASE`][] **Default:** `'main'`. - * `rate` {number} Number of pages to be transmitted in each batch of the backup. **Default:** `100`. + * `rate` {integer} Positive number of pages to be transmitted in each batch of the backup. **Default:** `100`. * `progress` {Function} An optional callback function that will be called after each backup step. The argument passed to this callback is an {Object} with `remainingPages` and `totalPages` properties, describing the current progress of the backup operation. @@ -1640,6 +1788,7 @@ callback function to indicate what type of operation is being authorized. +[Binding parameters]: #binding-parameters [Changesets and Patchsets]: https://www.sqlite.org/sessionintro.html#changesets_and_patchsets [Constants Passed To The Conflict Handler]: https://www.sqlite.org/session/c_changeset_conflict.html [Constants Returned From The Conflict Handler]: https://www.sqlite.org/session/c_changeset_abort.html @@ -1648,6 +1797,7 @@ callback function to indicate what type of operation is being authorized. [SQL injection]: https://en.wikipedia.org/wiki/SQL_injection [Type conversion between JavaScript and SQLite]: #type-conversion-between-javascript-and-sqlite [`ATTACH DATABASE`]: https://www.sqlite.org/lang_attach.html +[`ERR_INVALID_STATE`]: errors.md#err_invalid_state [`PRAGMA foreign_keys`]: https://www.sqlite.org/pragma.html#pragma_foreign_keys [`SQLITE_DBCONFIG_DEFENSIVE`]: https://www.sqlite.org/c3ref/c_dbconfig_defensive.html#sqlitedbconfigdefensive [`SQLITE_DETERMINISTIC`]: https://www.sqlite.org/c3ref/c_deterministic.html @@ -1674,6 +1824,7 @@ callback function to indicate what type of operation is being authorized. [`sqlite3_deserialize()`]: https://sqlite.org/c3ref/deserialize.html [`sqlite3_exec()`]: https://www.sqlite.org/c3ref/exec.html [`sqlite3_expanded_sql()`]: https://www.sqlite.org/c3ref/expanded_sql.html +[`sqlite3_finalize()`]: https://www.sqlite.org/c3ref/finalize.html [`sqlite3_get_autocommit()`]: https://sqlite.org/c3ref/get_autocommit.html [`sqlite3_last_insert_rowid()`]: https://www.sqlite.org/c3ref/last_insert_rowid.html [`sqlite3_load_extension()`]: https://www.sqlite.org/c3ref/load_extension.html @@ -1687,6 +1838,8 @@ callback function to indicate what type of operation is being authorized. [`sqlite3session_create()`]: https://www.sqlite.org/session/sqlite3session_create.html [`sqlite3session_delete()`]: https://www.sqlite.org/session/sqlite3session_delete.html [`sqlite3session_patchset()`]: https://www.sqlite.org/session/sqlite3session_patchset.html +[`statement.setAllowBareNamedParameters()`]: #statementsetallowbarenamedparametersenabled +[`statement.setAllowUnknownNamedParameters()`]: #statementsetallowunknownnamedparametersenabled [busy timeout]: https://sqlite.org/c3ref/busy_timeout.html [connection]: https://www.sqlite.org/c3ref/sqlite3.html [data types]: https://www.sqlite.org/datatype3.html diff --git a/doc/api/stream.md b/doc/api/stream.md index e83fbda8879a..bb1199a7365a 100644 --- a/doc/api/stream.md +++ b/doc/api/stream.md @@ -3643,13 +3643,18 @@ reader.read().then(({ value, done }) => { added: - v19.9.0 - v18.17.0 +changes: + - version: v22.0.0 + pr-url: https://github.com/nodejs/node/pull/52037 + description: bump default highWaterMark. --> * `objectMode` {boolean} * Returns: {integer} -Returns the default highWaterMark used by streams. -Defaults to `65536` (64 KiB), or `16` for `objectMode`. +Returns the default highWaterMark used by streams. Defaults to `16` for +`objectMode`. For byte streams, it defaults to `65536` (64 KiB) on non-Windows +platforms and `16384` (16 KiB) on Windows. ### `stream.setDefaultHighWaterMark(objectMode, value)` @@ -3779,7 +3784,7 @@ changes: * `options` {Object} * `highWaterMark` {number} Buffer level when [`stream.write()`][stream-write] starts returning `false`. **Default:** - `65536` (64 KiB), or `16` for `objectMode` streams. + See [`stream.getDefaultHighWaterMark()`][]. * `decodeStrings` {boolean} Whether to encode `string`s passed to [`stream.write()`][stream-write] to `Buffer`s (with the encoding specified in the [`stream.write()`][stream-write] call) before passing @@ -3812,7 +3817,7 @@ changes: -```js +```cjs const { Writable } = require('node:stream'); class MyWritable extends Writable { @@ -3824,18 +3829,18 @@ class MyWritable extends Writable { } ``` -Or, when using pre-ES6 style constructors: + -```js -const { Writable } = require('node:stream'); -const util = require('node:util'); +```mjs +import { Writable } from 'node:stream'; -function MyWritable(options) { - if (!(this instanceof MyWritable)) - return new MyWritable(options); - Writable.call(this, options); +class MyWritable extends Writable { + constructor(options) { + // Calls the stream.Writable() constructor. + super(options); + // ... + } } -util.inherits(MyWritable, Writable); ``` Or, using the simplified constructor approach: @@ -4153,7 +4158,7 @@ changes: * `options` {Object} * `highWaterMark` {number} The maximum [number of bytes][hwm-gotcha] to store in the internal buffer before ceasing to read from the underlying resource. - **Default:** `65536` (64 KiB), or `16` for `objectMode` streams. + **Default:** See [`stream.getDefaultHighWaterMark()`][]. * `encoding` {string} If specified, then buffers will be decoded to strings using the specified encoding. **Default:** `null`. * `objectMode` {boolean} Whether this stream should behave @@ -4185,20 +4190,6 @@ class MyReadable extends Readable { } ``` -Or, when using pre-ES6 style constructors: - -```js -const { Readable } = require('node:stream'); -const util = require('node:util'); - -function MyReadable(options) { - if (!(this instanceof MyReadable)) - return new MyReadable(options); - Readable.call(this, options); -} -util.inherits(MyReadable, Readable); -``` - Or, using the simplified constructor approach: ```js @@ -4517,7 +4508,7 @@ changes: -```js +```cjs const { Duplex } = require('node:stream'); class MyDuplex extends Duplex { @@ -4528,18 +4519,17 @@ class MyDuplex extends Duplex { } ``` -Or, when using pre-ES6 style constructors: + -```js -const { Duplex } = require('node:stream'); -const util = require('node:util'); +```mjs +import { Duplex } from 'node:stream'; -function MyDuplex(options) { - if (!(this instanceof MyDuplex)) - return new MyDuplex(options); - Duplex.call(this, options); +class MyDuplex extends Duplex { + constructor(options) { + super(options); + // ... + } } -util.inherits(MyDuplex, Duplex); ``` Or, using the simplified constructor approach: @@ -4714,7 +4704,7 @@ output on the `Readable` side is not consumed. -```js +```cjs const { Transform } = require('node:stream'); class MyTransform extends Transform { @@ -4725,18 +4715,17 @@ class MyTransform extends Transform { } ``` -Or, when using pre-ES6 style constructors: + -```js -const { Transform } = require('node:stream'); -const util = require('node:util'); +```mjs +import { Transform } from 'node:stream'; -function MyTransform(options) { - if (!(this instanceof MyTransform)) - return new MyTransform(options); - Transform.call(this, options); +class MyTransform extends Transform { + constructor(options) { + super(options); + // ... + } } -util.inherits(MyTransform, Transform); ``` Or, using the simplified constructor approach: @@ -5094,6 +5083,7 @@ contain multi-byte characters. [`stream.cork()`]: #writablecork [`stream.duplexPair()`]: #streamduplexpairoptions [`stream.finished()`]: #streamfinishedstream-options-callback +[`stream.getDefaultHighWaterMark()`]: #streamgetdefaulthighwatermarkobjectmode [`stream.pipe()`]: #readablepipedestination-options [`stream.pipeline()`]: #streampipelinesource-transforms-destination-callback [`stream.uncork()`]: #writableuncork diff --git a/doc/api/synopsis.md b/doc/api/synopsis.md index 24bb35e08f8c..85b2b4cf7470 100644 --- a/doc/api/synopsis.md +++ b/doc/api/synopsis.md @@ -15,15 +15,8 @@ Please see the [Command-line options][] document for more information. An example of a [web server][] written with Node.js which responds with `'Hello, World!'`: -Commands in this document start with `$` or `>` to replicate how they would -appear in a user's terminal. Do not include the `$` and `>` characters. They are -there to show the start of each command. - -Lines that don't start with `$` or `>` character show the output of the previous -command. - First, make sure to have downloaded and installed Node.js. See -[Installing Node.js via package manager][] for further install information. +[Installing Node.js][] for further install information. Now, create an empty project folder called `projects`, then navigate into it. @@ -90,5 +83,5 @@ If the browser displays the string `Hello, World!`, that indicates the server is working. [Command-line options]: cli.md#options -[Installing Node.js via package manager]: https://nodejs.org/en/download/package-manager/ +[Installing Node.js]: https://nodejs.org/en/download [web server]: http.md diff --git a/doc/api/url.md b/doc/api/url.md index bdc27144fa3a..7759c03a15e7 100644 --- a/doc/api/url.md +++ b/doc/api/url.md @@ -830,6 +830,7 @@ console.log(myPattern.exec('https://nodejs.org/docs/latest/api/dns.html')); * `input` {string | Object} A URL or URL parts * `baseURL` {string | undefined} A base URL string +* Returns {boolean} Input can be a string or an object providing the individual URL parts. The object members can be any of `protocol`, `username`, `password`, `hostname`, diff --git a/doc/api/util.md b/doc/api/util.md index bf61e9f89322..e35e78982bb0 100644 --- a/doc/api/util.md +++ b/doc/api/util.md @@ -1896,6 +1896,18 @@ console.log(JSON.stringify(myMIMES)); // Prints: ["image/png", "image/gif"] ``` +### `MIMEType.parse(string)` + + + +* `string` {string} The input MIME to parse +* Returns: {MIMEType|null} + +Attempts to parse the given `string` as a MIMEType. If the string cannot be +parsed, `null` is returned. + ## Class: `util.MIMEParams`