From 3de2ca0d97a2309909d0915057eb321b6963f84b Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Mon, 29 Jun 2026 22:40:34 +0800 Subject: [PATCH 01/85] adj: null-check fixup_func before calling in l2/nsh midchain nodes The main data-path in adj_dp.h guards fixup_func with a null check, but adj_l2.c and adj_nsh.c call it unconditionally. When LCP/netlink errors tear down an adjacency, fixup_func can be NULL, leading to SIGSEGV at address 0x0 in adj_l2_midchain_node_fn_x86_64_v4. Add the same null check already present in adj_dp.h. linux-cp: downgrade neighbor-deletion log for local addresses When the kernel sends RTM_DELNEIGH for local interface addresses (e.g. VRRP VIPs or bird-managed IPs), ip_neighbor_del() returns VNET_API_ERROR_NO_SUCH_ENTRY because those addresses are not ARP neighbors in VPP. This is expected, not an error. Log it at DBG level instead of ERROR to avoid flooding the journal. Type: fix Signed-off-by: sunoaki --- src/plugins/linux-cp/lcp_router.c | 8 +++++++- src/vnet/adj/adj_l2.c | 2 +- src/vnet/adj/adj_nsh.c | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/plugins/linux-cp/lcp_router.c b/src/plugins/linux-cp/lcp_router.c index a2bd4c977f79..4110f7487894 100644 --- a/src/plugins/linux-cp/lcp_router.c +++ b/src/plugins/linux-cp/lcp_router.c @@ -780,7 +780,13 @@ lcp_router_neigh_del (struct rtnl_neigh *rn) rv = ip_neighbor_del (&nh, sw_if_index); - if (rv) + if (rv == VNET_API_ERROR_NO_SUCH_ENTRY) + { + LCP_ROUTER_DBG ( + "neighbor already absent: %U %U", format_ip_address, &nh, + format_vnet_sw_if_index_name, vnet_get_main (), sw_if_index); + } + else if (rv) { LCP_ROUTER_ERROR ( "Failed to delete neighbor: %U %U", format_ip_address, &nh, diff --git a/src/vnet/adj/adj_l2.c b/src/vnet/adj/adj_l2.c index 7e9e985ecb71..7bfc42c38ef3 100644 --- a/src/vnet/adj/adj_l2.c +++ b/src/vnet/adj/adj_l2.c @@ -105,7 +105,7 @@ adj_l2_rewrite_inline (vlib_main_t * vm, p0->current_length += rw_len0; tx_sw_if_index0 = adj0[0].rewrite_header.sw_if_index; - if (is_midchain) + if (is_midchain && adj0->sub_type.midchain.fixup_func) { adj0->sub_type.midchain.fixup_func( vm, adj0, p0, diff --git a/src/vnet/adj/adj_nsh.c b/src/vnet/adj/adj_nsh.c index 1480f3fa54a7..19728320af58 100644 --- a/src/vnet/adj/adj_nsh.c +++ b/src/vnet/adj/adj_nsh.c @@ -101,7 +101,7 @@ adj_nsh_rewrite_inline (vlib_main_t * vm, p0->current_length += rw_len0; tx_sw_if_index0 = adj0[0].rewrite_header.sw_if_index; - if (is_midchain) + if (is_midchain && adj0->sub_type.midchain.fixup_func) { adj0->sub_type.midchain.fixup_func( vm, adj0, p0, From e19177b06e82f08e7d88a3623c4be86cf8f9c014 Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 30 Jun 2026 00:48:08 +0800 Subject: [PATCH 02/85] github: add Debian 13 Trixie DEB build workflow --- .github/workflows/build-deb-trixie.yml | 106 +++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 .github/workflows/build-deb-trixie.yml diff --git a/.github/workflows/build-deb-trixie.yml b/.github/workflows/build-deb-trixie.yml new file mode 100644 index 000000000000..ba01428cf35d --- /dev/null +++ b/.github/workflows/build-deb-trixie.yml @@ -0,0 +1,106 @@ +name: Build VPP DEB for Debian 13 (Trixie) + +on: + push: + branches: [fix/*] + workflow_dispatch: + inputs: + release: + description: "Publish as GitHub Release" + type: boolean + default: false + tag: + description: "Release tag (if publishing)" + type: string + default: "" + +jobs: + build: + runs-on: ubuntu-24.04 + container: + image: debian:trixie + env: + CCACHE_COMPRESS: "true" + CCACHE_MAXSIZE: 2G + steps: + - name: Install base tools + run: | + apt-get update -qq + apt-get install -y -qq sudo make git + + - name: Clone repository + run: | + git clone --depth 1 --branch "$GITHUB_REF_NAME" \ + "https://github.com/$GITHUB_REPOSITORY.git" . + + - name: Compute short SHA + id: vars + shell: bash + run: echo "short_sha=$(git rev-parse --short=7 HEAD)" >> $GITHUB_OUTPUT + + - name: Restore apt cache + uses: actions/cache/restore@v4 + with: + path: /var/cache/apt + key: apt-debian13-${{ runner.arch }} + restore-keys: | + apt-debian13- + + - name: Install VPP build dependencies + run: | + sed -i '496s/./&-/' Makefile + make UNATTENDED=yes install-dep + + - name: Save apt cache + uses: actions/cache/save@v4 + with: + path: /var/cache/apt + key: apt-debian13-${{ runner.arch }} + + - name: Restore ccache + uses: actions/cache/restore@v4 + with: + path: build-root/.ccache + key: ccache-debian13-${{ runner.arch }}-${{ github.ref_name }} + restore-keys: | + ccache-debian13-${{ runner.arch }}- + + - name: Build VPP DEB packages + run: | + git config --global --add safe.directory "$PWD" + git fetch --tags --force 2>/dev/null || true + if ! git describe --tags --long --match 'v*' 2>/dev/null; then + echo "v26.10-0-g$(git rev-parse --short HEAD)" > src/scripts/.version + fi + ccache -z + export VPP_EXTRA_CMAKE_ARGS="\ + -DCMAKE_C_COMPILER_LAUNCHER=ccache \ + -DCMAKE_CXX_COMPILER_LAUNCHER=ccache" + make pkg-deb + ccache -s + + - name: Save ccache + uses: actions/cache/save@v4 + with: + path: build-root/.ccache + key: ccache-debian13-${{ runner.arch }}-${{ github.ref_name }} + + - name: Upload DEB artifacts + uses: actions/upload-artifact@v4 + with: + name: vpp-debian13-trixie + path: build-root/*.deb + + - name: Publish Release + if: ${{ github.event_name == 'workflow_dispatch' && inputs.release }} + uses: softprops/action-gh-release@v2 + with: + files: build-root/*.deb + tag_name: ${{ inputs.tag || format('vpp-trixie-{0}', github.run_number) }} + name: "VPP ${{ steps.vars.outputs.short_sha }} for Debian 13" + body: | + VPP build from commit ${{ github.sha }} + - Branch: ${{ github.ref_name }} + - Build date: ${{ github.event.repository.updated_at }} + draft: false + prerelease: true From 04290a66ca68483bf710def6e931a8a2c9d9c383 Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 30 Jun 2026 00:50:54 +0800 Subject: [PATCH 03/85] github: remove github2gerrit mirror workflow --- .github/workflows/github2gerrit.yaml | 68 ---------------------------- 1 file changed, 68 deletions(-) delete mode 100644 .github/workflows/github2gerrit.yaml diff --git a/.github/workflows/github2gerrit.yaml b/.github/workflows/github2gerrit.yaml deleted file mode 100644 index ec8a9808e837..000000000000 --- a/.github/workflows/github2gerrit.yaml +++ /dev/null @@ -1,68 +0,0 @@ ---- -# SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: 2025 The Linux Foundation - -name: 'GitHub2Gerrit' - -# yamllint disable-line rule:truthy -on: - workflow_dispatch: - inputs: - preserve_github_prs: - description: "Do NOT close GitHub PRs after pushing to Gerrit" - required: false - default: false - type: boolean - allow_duplicates: - description: "Allow duplicate changes to be raised in Gerrit" - required: false - default: false - type: boolean - - pull_request_target: - types: [opened, reopened, edited, synchronize] - branches: - - master - - main - -concurrency: - group: "${{ github.workflow }}-${{ github.run_id }}" - cancel-in-progress: true - -jobs: - github2gerrit: - name: 'GitHub2Gerrit' - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: write - issues: write - timeout-minutes: 12 - steps: - # Harden the runner used by this workflow - # yamllint disable-line rule:line-length - - uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2 - name: 'Harden runner' - with: - egress-policy: audit - - - name: 'Checkout repository' - # yamllint disable-line rule:line-length - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v6.0.2 - with: - fetch-depth: 10 - ref: ${{ github.event.pull_request.head.sha || github.sha }} - - - name: 'Run GitHub2Gerrit Action' - # yamllint disable-line rule:line-length - uses: lfreleng-actions/github2gerrit-action@7c1ba15a75dd86619139a81933f4432648d97af5 # v1.0.5 - with: - USE_PR_AS_COMMIT: true - ALLOW_DUPLICATES: ${{ inputs.allow_duplicates }} - PRESERVE_GITHUB_PRS: ${{ inputs.preserve_github_prs }} - ISSUE_ID_LOOKUP_JSON: ${{ vars.ISSUE_ID_LOOKUP_JSON }} - GERRIT_SSH_PRIVKEY_G2G: ${{ secrets.GERRIT_SSH_PRIVKEY_G2G }} - GERRIT_SSH_USER_G2G: ${{ vars.GERRIT_SSH_USER_G2G }} - GERRIT_SSH_USER_G2G_EMAIL: ${{ vars.GERRIT_SSH_USER_G2G_EMAIL }} - GERRIT_KNOWN_HOSTS: ${{ vars.GERRIT_KNOWN_HOSTS }} - VERBOSE: true From 20371f5c0e80a2b881e3c45912426788216e3695 Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 30 Jun 2026 00:51:49 +0800 Subject: [PATCH 04/85] github: remove all Gerrit-related workflows --- .github/workflows/gerrit-comment-handler.yml | 219 ------------ .github/workflows/gerrit-merge.yml | 253 ------------- .github/workflows/gerrit-verify.yml | 355 ------------------- 3 files changed, 827 deletions(-) delete mode 100644 .github/workflows/gerrit-comment-handler.yml delete mode 100644 .github/workflows/gerrit-merge.yml delete mode 100644 .github/workflows/gerrit-verify.yml diff --git a/.github/workflows/gerrit-comment-handler.yml b/.github/workflows/gerrit-comment-handler.yml deleted file mode 100644 index e28e784f207c..000000000000 --- a/.github/workflows/gerrit-comment-handler.yml +++ /dev/null @@ -1,219 +0,0 @@ -name: gerrit-comment-handler - -# yamllint disable-line rule:truthy -on: - workflow_dispatch: - inputs: - FDIO_NAMESPACE: - description: "Executor Namespace (prod|sandbox)" - required: false - type: choice - options: - - sandbox - - prod - default: prod - GERRIT_BRANCH: - description: "Branch that change is against" - required: true - type: string - GERRIT_CHANGE_ID: - description: "The ID for the change" - required: true - type: string - GERRIT_CHANGE_NUMBER: - description: "The Gerrit number" - required: true - type: string - GERRIT_CHANGE_URL: - description: "URL to the change" - required: true - type: string - GERRIT_EVENT_TYPE: - description: "Type of Gerrit event" - required: true - type: string - GERRIT_PATCHSET_NUMBER: - description: "The patch number for the change" - required: true - type: string - GERRIT_PATCHSET_REVISION: - description: "The revision sha" - required: true - type: string - GERRIT_PROJECT: - description: "Project in Gerrit" - required: true - type: string - GERRIT_REFSPEC: - description: "Gerrit refspec of change" - required: true - type: string - GERRIT_COMMENT: - description: "Full command line from Gerrit comment" - required: true - type: string - -concurrency: - group: ${{ inputs.GERRIT_PROJECT }}-${{ inputs.GERRIT_BRANCH }}-${{ inputs.GERRIT_CHANGE_NUMBER }}-${{ inputs.FDIO_NAMESPACE }} - cancel-in-progress: true - -permissions: - contents: read - -jobs: - notify-start: - runs-on: ubuntu-latest - outputs: - log_prefix: ${{ steps.start-time.outputs.log_prefix }} - - steps: - - name: Start Time - id: start-time - run: | - set -euxo pipefail - timestamp=$(date -u +%Y_%m_%d_%H%M%S_UTC) - log_prefix="gha-vpp-gerrit-patchset/csit-perf-${{ inputs.GERRIT_BRANCH }}-${timestamp}-gerrit-${{ inputs.GERRIT_CHANGE_NUMBER }}-${{ inputs.GERRIT_PATCHSET_NUMBER}}" - echo "log_prefix=$log_prefix" >> "$GITHUB_OUTPUT" - echo "Gerrit Change: ${{ inputs.GERRIT_CHANGE_URL }}" - - - name: Job Details - run: | - { - echo "### Gerrit Comment Handler" - echo - echo "| Field | Value |" - echo "| --- | --- |" - echo "| Gerrit change | ${{ inputs.GERRIT_CHANGE_URL }}/${{ inputs.GERRIT_PATCHSET_NUMBER }} |" - echo "| Branch | ${{ inputs.GERRIT_BRANCH }} |" - echo "| Gerrit Change ID | ${{ inputs.GERRIT_CHANGE_ID }} |" - echo "| Patchset Revision | ${{ inputs.GERRIT_PATCHSET_REVISION }} |" - echo "| Refspec | ${{ inputs.GERRIT_REFSPEC }} |" - echo "| Gerrit Event | ${{ inputs.GERRIT_EVENT_TYPE }} |" - echo "| Gerrit Comment | ${{ inputs.GERRIT_COMMENT }} |" - echo "| Namespace | ${{ inputs.FDIO_NAMESPACE }} |" - } >> "$GITHUB_STEP_SUMMARY" - - - name: Clear Gerrit vote - # yamllint disable-line rule:line-length - uses: lfreleng-actions/gerrit-review-action@a5de1d5bf17c2603ae81544dc2a455017b454ec8 # 1.1.1 - with: - host: ${{ vars.GERRIT_SERVER }} - username: ${{ vars.GERRIT_SSH_REQUIRED_USER }} - key: ${{ secrets.GERRIT_SSH_REQUIRED_PRIVKEY }} - known_hosts: ${{ vars.GERRIT_KNOWN_HOSTS }} - gerrit-change-number: ${{ inputs.GERRIT_CHANGE_NUMBER }} - gerrit-patchset-number: ${{ inputs.GERRIT_PATCHSET_NUMBER }} - vote-type: clear - comment-only: true - - dispatch: - runs-on: ubuntu-latest - - outputs: - matrix: ${{ steps.set-matrix.outputs.matrix }} - params: ${{ steps.set-matrix.outputs.params }} - - steps: - - name: Prepare workflow selection matrix - id: set-matrix - uses: fdio/csit/.github/actions/csit-comment-dispatch@1f3de8c4266cc118bbf3e108dd0f33a42b0cc70f - with: - gerrit_comment: ${{ github.event.inputs.GERRIT_COMMENT }} - - vpp-csit-verify-handler: - needs: dispatch - # yamllint disable-line rule:line-length - name: vpp-csit-perf-verify-${{ matrix.node }} - runs-on: - - self-hosted - - nomad - - fdio:arch=${{ matrix.executor_arch }} - - fdio:class=builder - - fdio:size=csit - - fdio:namespace=${{ inputs.FDIO_NAMESPACE || 'prod' }} - - fdio:os=${{ matrix.os }} - - fdio:workflow=${{ github.run_id }} - timeout-minutes: 1440 - - strategy: - matrix: ${{ fromJSON(needs.dispatch.outputs.matrix) }} - - env: - # yamllint disable-line rule:line-length - BUILD_TAG: vpp-csit-verify-perf-${{ github.event.inputs.GERRIT_BRANCH }}-${{ matrix.os }}-${{ matrix.node }}-${{ github.run_id }} - # yamllint disable-line rule:line-length - JOB_NAME: vpp-csit-verify-perf-${{ github.event.inputs.GERRIT_BRANCH }}-${{ matrix.os }}-${{ matrix.executor_arch }}-${{ matrix.node }} - WORKSPACE: /scratch/docker-build/vpp - CSIT_WORKSPACE: /scratch/docker-build/csit - GERRIT_BRANCH: ${{ github.event.inputs.GERRIT_BRANCH }} - GERRIT_CHANGE_URL: ${{ github.event.inputs.GERRIT_CHANGE_URL }} - GERRIT_PROJECT: ${{ github.event.inputs.GERRIT_PROJECT }} - GERRIT_REFSPEC: ${{ github.event.inputs.GERRIT_REFSPEC }} - GERRIT_CHANGE_NUMBER: ${{ github.event.inputs.GERRIT_CHANGE_NUMBER }} - GERRIT_PATCHSET_NUMBER: ${{ github.event.inputs.GERRIT_PATCHSET_NUMBER }} - LOG_PREFIX: ${{ github.event.inputs.LOG_PREFIX }} - FDIO_NAMESPACE: ${{ github.event.inputs.FDIO_NAMESPACE || 'prod' }} - LOG_NAME: "" - LOG_DIR: "" - TUI_LINE: "*******************************************************************" - GERRIT_EVENT_TYPE: comment-added - GERRIT_EVENT_COMMENT_TEXT: ${{ needs.dispatch.outputs.params }} - - steps: - - name: Gerrit Checkout VPP - # yamllint disable-line rule:line-length - uses: fdio/vpp/.github/actions/vpp-checkout-gerrit-change@27fc2aa27f57c8f7055e8f1b6e5a9f41b0315873 - with: - WORKSPACE: ${{ env.WORKSPACE }} - GERRIT_BRANCH: ${{ env.GERRIT_BRANCH }} - GERRIT_REFSPEC: ${{ env.GERRIT_REFSPEC }} - TUI_LINE: ${{ env.TUI_LINE }} - - - name: Gerrit Checkout CSIT - # yamllint disable-line rule:line-length - uses: fdio/vpp/.github/actions/vpp-checkout-gerrit-change@27fc2aa27f57c8f7055e8f1b6e5a9f41b0315873 - with: - WORKSPACE: ${{ env.CSIT_WORKSPACE }} - GERRIT_BRANCH: master - GERRIT_REFSPEC: "" - TUI_LINE: ${{ env.TUI_LINE }} - - - name: Run CSIT Bootstrap - uses: fdio/csit/.github/actions/csit-bootstrap@6e2e8dd6eae1a9297bfcc3a1630be181191a0553 - with: - bootstrap_script: per_patch_perf.sh - path: ${{ env.CSIT_WORKSPACE }} - with_oper: true - - - name: AWS S3 Publish Logs - if: ${{ !cancelled() && (success() || failure()) }} - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/aws-s3-publish-logs@1b8eff3ffd9d355b28052be9ad22acc57b3430b5 - with: - aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws_region: ${{ vars.AWS_REGION }} - s3_bucket: ${{ vars.S3_BUCKET }} - # yamllint disable-line rule:line-length - s3_path: vpp-csit-verify-perf-${{ matrix.node }}/${{ github.run_id }} - archives_path: ${{ env.CSIT_WORKSPACE }}/archives - - vote: - if: ${{ always() }} - needs: - - notify-start - - vpp-csit-verify-handler - runs-on: ubuntu-latest - steps: - - uses: technote-space/workflow-conclusion-action@45ce8e0eb155657ab8ccf346ade734257fd196a5 # v3.0.3 archived Nov 16, 2023 - - name: Set vote - uses: lfit/gerrit-review-action@a5de1d5bf17c2603ae81544dc2a455017b454ec8 # 1.1.1 - with: - host: ${{ vars.GERRIT_SERVER }} - username: ${{ vars.GERRIT_SSH_REQUIRED_USER }} - key: ${{ secrets.GERRIT_SSH_REQUIRED_PRIVKEY }} - known_hosts: ${{ vars.GERRIT_KNOWN_HOSTS }} - gerrit-change-number: ${{ github.event.inputs.GERRIT_CHANGE_NUMBER }} - gerrit-patchset-number: ${{ github.event.inputs.GERRIT_PATCHSET_NUMBER }} - vote-type: ${{ env.WORKFLOW_CONCLUSION }} - comment-only: true diff --git a/.github/workflows/gerrit-merge.yml b/.github/workflows/gerrit-merge.yml deleted file mode 100644 index a96fcdef3b77..000000000000 --- a/.github/workflows/gerrit-merge.yml +++ /dev/null @@ -1,253 +0,0 @@ -name: gerrit-merge - -on: - workflow_dispatch: - inputs: - FDIO_NAMESPACE: - description: "Executor Namespace (prod|sandbox)" - required: false - type: choice - options: - - sandbox - - prod - default: prod - GERRIT_BRANCH: - description: "Branch that change is against" - required: true - type: string - GERRIT_CHANGE_ID: - description: "The ID for the change" - required: true - type: string - GERRIT_CHANGE_NUMBER: - description: "The Gerrit number" - required: true - type: string - GERRIT_CHANGE_URL: - description: "URL to the change" - required: true - type: string - GERRIT_EVENT_TYPE: - description: "Type of Gerrit event" - required: true - type: string - GERRIT_PATCHSET_NUMBER: - description: "The patch number for the change" - required: true - type: string - GERRIT_PATCHSET_REVISION: - description: "The revision sha" - required: true - type: string - GERRIT_PROJECT: - description: "Project in Gerrit" - required: true - type: string - GERRIT_REFSPEC: - description: "Gerrit refspec of change" - required: true - type: string - -defaults: - run: - shell: bash - -permissions: - contents: read - actions: read - -concurrency: - group: ${{ inputs.GERRIT_PROJECT }}-${{ inputs.GERRIT_BRANCH }}-${{ inputs.GERRIT_CHANGE_NUMBER }}-${{ inputs.FDIO_NAMESPACE }} - cancel-in-progress: true - -jobs: - start-merge: - name: Start Merge - runs-on: ubuntu-latest - outputs: - log_prefix: ${{ steps.start-time.outputs.log_prefix }} - steps: - - name: Start Time - id: start-time - run: | - set -euxo pipefail - timestamp=$(date -u +%Y_%m_%d_%H%M%S_UTC) - branch=${{ inputs.GERRIT_BRANCH }} - branch=${branch/\//-} - log_prefix="gha-vpp-gerrit-patchset/merge-${branch}-${timestamp}-gerrit-${{ inputs.GERRIT_CHANGE_NUMBER }}-${{ inputs.GERRIT_PATCHSET_NUMBER}}" - echo "log_prefix=$log_prefix" >> "$GITHUB_OUTPUT" - echo "Gerrit Change: ${{ inputs.GERRIT_CHANGE_URL }}" - echo "Gerrit Event Type: ${{ inputs.GERRIT_EVENT_TYPE }}" - - - name: Job Details - run: | - { - echo "### Gerrit Merge Handler" - echo - echo "| Field | Value |" - echo "| --- | --- |" - echo "| Gerrit change | ${{ inputs.GERRIT_CHANGE_URL }} |" - echo "| Branch | ${{ inputs.GERRIT_BRANCH }} |" - echo "| Gerrit Change ID | ${{ inputs.GERRIT_CHANGE_ID }} |" - echo "| Patchset Revision | ${{ inputs.GERRIT_PATCHSET_REVISION }} |" - echo "| Refspec | ${{ inputs.GERRIT_REFSPEC }} |" - echo "| Gerrit Event | ${{ inputs.GERRIT_EVENT_TYPE }} |" - echo "| Gerrit Comment | ${{ inputs.GERRIT_COMMENT }} |" - echo "| Namespace | ${{ inputs.FDIO_NAMESPACE }} |" - } >> "$GITHUB_STEP_SUMMARY" - - - name: Clear Gerrit Vote - # yamllint disable-line rule:line-length - uses: lfreleng-actions/gerrit-review-action@a5de1d5bf17c2603ae81544dc2a455017b454ec8 # 1.1.1 - with: - host: ${{ vars.GERRIT_SERVER }} - username: ${{ vars.GERRIT_SSH_REQUIRED_USER }} - key: ${{ secrets.GERRIT_SSH_REQUIRED_PRIVKEY }} - known_hosts: ${{ vars.GERRIT_KNOWN_HOSTS }} - gerrit-change-number: ${{ inputs.GERRIT_CHANGE_NUMBER }} - gerrit-patchset-number: ${{ inputs.GERRIT_PATCHSET_NUMBER }} - vote-type: clear - comment-only: true # Just set a comment, do not vote on gerrit - - merge-maketest: - name: Merge Maketest - needs: - - start-merge - uses: fdio/vpp/.github/workflows/vpp-merge-maketest.yml@c316a780f8f25c5d2386ef8fa085e90338300e3a - with: - FDIO_NAMESPACE: ${{ inputs.FDIO_NAMESPACE }} - GERRIT_BRANCH: ${{ inputs.GERRIT_BRANCH }} - GERRIT_CHANGE_ID: ${{ inputs.GERRIT_CHANGE_ID }} - GERRIT_CHANGE_NUMBER: ${{ inputs.GERRIT_CHANGE_NUMBER }} - GERRIT_CHANGE_URL: ${{ inputs.GERRIT_CHANGE_URL }} - GERRIT_EVENT_TYPE: ${{ inputs.GERRIT_EVENT_TYPE }} - GERRIT_PATCHSET_NUMBER: ${{ inputs.GERRIT_PATCHSET_NUMBER }} - GERRIT_PATCHSET_REVISION: ${{ inputs.GERRIT_PATCHSET_REVISION }} - GERRIT_PROJECT: ${{ inputs.GERRIT_PROJECT }} - GERRIT_REFSPEC: refs/heads/${{ inputs.GERRIT_BRANCH }} - LOG_PREFIX: ${{ needs.start-merge.outputs.log_prefix }} - secrets: inherit - - detect-docs-changes: - name: Detect Docs Changes - needs: - - start-merge - runs-on: - - self-hosted - - nomad - - fdio:arch=x86_64 - - fdio:class=builder - - fdio:namespace=${{ inputs.FDIO_NAMESPACE || 'prod' }} - - fdio:os=ubuntu2404 - - fdio:size=csit - - fdio:workflow=${{ github.run_id }} - outputs: - docs_changed: ${{ steps.docs-changed.outputs.docs_changed }} - env: - WORKSPACE: /scratch/docker-build/vpp - GERRIT_BRANCH: ${{ inputs.GERRIT_BRANCH }} - GERRIT_REFSPEC: ${{ inputs.GERRIT_REFSPEC }} - TUI_LINE: "*******************************************************************" - steps: - - name: Setup Environment - id: setup-executor-env - continue-on-error: true - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/setup-executor-env@b728d1589228e2891a4241a1bba362d76895c225 - - - name: Checkout VPP Gerrit Change - id: checkout-vpp - continue-on-error: true - # yamllint disable-line rule:line-length - uses: fdio/vpp/.github/actions/vpp-checkout-gerrit-change@09552c71554236d36c1eb51a1740a366019bc1a5 - with: - WORKSPACE: ${{ env.WORKSPACE }} - GERRIT_BRANCH: ${{ env.GERRIT_BRANCH }} - GERRIT_REFSPEC: ${{ env.GERRIT_REFSPEC }} - TUI_LINE: ${{ env.TUI_LINE }} - - - name: Detect docs changes - id: docs-changed - if: ${{ always() }} - env: - CHECKOUT_OUTCOME: ${{ steps.checkout-vpp.outcome }} - run: | - set -euxo pipefail - - docs_changed="true" - if [[ "$CHECKOUT_OUTCOME" == "success" ]]; then - cd "$WORKSPACE" - if git diff-tree --no-commit-id --name-only -r --root HEAD | grep -Eq '(^docs/|\.rst$)'; then - docs_changed="true" - else - docs_changed="false" - fi - else - echo "warn: unable to checkout '${GERRIT_REFSPEC}'; defaulting docs_changed=true" >&2 - fi - - echo "docs_changed=$docs_changed" >> "$GITHUB_OUTPUT" - - merge-docs: - name: Merge Docs - needs: - - start-merge - - detect-docs-changes - if: ${{ needs.detect-docs-changes.outputs.docs_changed == 'true' }} - uses: fdio/vpp/.github/workflows/vpp-merge-docs.yml@e5c8759e531c155d1aa983fcb0fe84fd1f004c88 - with: - FDIO_NAMESPACE: ${{ inputs.FDIO_NAMESPACE }} - GERRIT_BRANCH: ${{ inputs.GERRIT_BRANCH }} - GERRIT_CHANGE_ID: ${{ inputs.GERRIT_CHANGE_ID }} - GERRIT_CHANGE_NUMBER: ${{ inputs.GERRIT_CHANGE_NUMBER }} - GERRIT_CHANGE_URL: ${{ inputs.GERRIT_CHANGE_URL }} - GERRIT_EVENT_TYPE: ${{ inputs.GERRIT_EVENT_TYPE }} - GERRIT_PATCHSET_NUMBER: ${{ inputs.GERRIT_PATCHSET_NUMBER }} - GERRIT_PATCHSET_REVISION: ${{ inputs.GERRIT_PATCHSET_REVISION }} - GERRIT_PROJECT: ${{ inputs.GERRIT_PROJECT }} - GERRIT_REFSPEC: refs/heads/${{ inputs.GERRIT_BRANCH }} - LOG_PREFIX: ${{ needs.start-merge.outputs.log_prefix }} - secrets: inherit - - final-vote: - name: Vote - if: ${{ always() }} - needs: - - start-merge - - detect-docs-changes - - merge-maketest - - merge-docs - runs-on: ubuntu-latest - env: - GERRIT_CHANGE_NUMBER: ${{ inputs.GERRIT_CHANGE_NUMBER }} - GERRIT_PATCHSET_NUMBER: ${{ inputs.GERRIT_PATCHSET_NUMBER }} - steps: - - name: Collect workflow results - id: overall - run: | - set -euxo pipefail - docs_ok="true" - if [[ "${{ needs.detect-docs-changes.outputs.docs_changed }}" == "true" ]]; then - [[ "${{ needs.merge-docs.result }}" == "success" ]] || docs_ok="false" - fi - - if [[ "${{ needs.start-merge.result }}" == "success" && \ - "${{ needs.merge-maketest.result }}" == "success" && \ - "$docs_ok" == "true" ]] ; then - echo "result=success" >> "$GITHUB_OUTPUT" - else - echo "result=failure" >> "$GITHUB_OUTPUT" - fi - - - name: Gerrit Vote Disabled - # yamllint disable-line rule:line-length - uses: lfreleng-actions/gerrit-review-action@a5de1d5bf17c2603ae81544dc2a455017b454ec8 # 1.1.1 - with: - host: ${{ vars.GERRIT_SERVER }} - username: ${{ vars.GERRIT_SSH_USER }} - key: ${{ secrets.GERRIT_SSH_PRIVKEY }} - known_hosts: ${{ vars.GERRIT_KNOWN_HOSTS }} - gerrit-change-number: ${{ env.GERRIT_CHANGE_NUMBER }} - gerrit-patchset-number: ${{ env.GERRIT_PATCHSET_NUMBER }} - vote-type: ${{ steps.overall.outputs.result }} - comment-only: true # Just set a comment, do not vote on gerrit diff --git a/.github/workflows/gerrit-verify.yml b/.github/workflows/gerrit-verify.yml deleted file mode 100644 index 67353f5404d0..000000000000 --- a/.github/workflows/gerrit-verify.yml +++ /dev/null @@ -1,355 +0,0 @@ -name: gerrit-verify - -on: - workflow_dispatch: - inputs: - FDIO_NAMESPACE: - description: "Executor Namespace (prod|sandbox)" - required: false - type: choice - options: - - sandbox - - prod - default: prod - GERRIT_BRANCH: - description: "Branch that change is against" - required: true - type: string - GERRIT_CHANGE_ID: - description: "The ID for the change" - required: true - type: string - GERRIT_CHANGE_NUMBER: - description: "The Gerrit number" - required: true - type: string - GERRIT_CHANGE_URL: - description: "URL to the change" - required: true - type: string - GERRIT_EVENT_TYPE: - description: "Type of Gerrit event" - required: true - type: string - GERRIT_PATCHSET_NUMBER: - description: "The patch number for the change" - required: true - type: string - GERRIT_PATCHSET_REVISION: - description: "The revision sha" - required: true - type: string - GERRIT_PROJECT: - description: "Project in Gerrit" - required: true - type: string - GERRIT_REFSPEC: - description: "Gerrit refspec of change" - required: true - type: string - -defaults: - run: - shell: bash - -permissions: - contents: read - actions: read - -concurrency: - group: ${{ inputs.GERRIT_PROJECT }}-${{ inputs.GERRIT_BRANCH }}-${{ inputs.GERRIT_CHANGE_NUMBER }}-${{ inputs.FDIO_NAMESPACE }} - cancel-in-progress: true - -jobs: - start-verify: - name: Start Verify - runs-on: ubuntu-latest - outputs: - log_prefix: ${{ steps.start-time.outputs.log_prefix }} - - steps: - - name: Start Time - id: start-time - run: | - set -euxo pipefail - timestamp=$(date -u +%Y_%m_%d_%H%M%S_UTC) - branch=${{ inputs.GERRIT_BRANCH }} - branch=${branch/\//-} - log_prefix="gha-vpp-gerrit-patchset/verify-${branch}-${timestamp}-gerrit-${{ inputs.GERRIT_CHANGE_NUMBER }}-${{ inputs.GERRIT_PATCHSET_NUMBER}}" - echo "log_prefix=$log_prefix" >> "$GITHUB_OUTPUT" - echo "Gerrit Change: ${{ inputs.GERRIT_CHANGE_URL }}" - - - name: Job Details - run: | - { - echo "### Gerrit Verify Handler" - echo - echo "| Field | Value |" - echo "| --- | --- |" - echo "| Gerrit change | ${{ inputs.GERRIT_CHANGE_URL }}/${{ inputs.GERRIT_PATCHSET_NUMBER }} |" - echo "| Branch | ${{ inputs.GERRIT_BRANCH }} |" - echo "| Gerrit Change ID | ${{ inputs.GERRIT_CHANGE_ID }} |" - echo "| Patchset Revision | ${{ inputs.GERRIT_PATCHSET_REVISION }} |" - echo "| Refspec | ${{ inputs.GERRIT_REFSPEC }} |" - echo "| Gerrit Event | ${{ inputs.GERRIT_EVENT_TYPE }} |" - echo "| Gerrit Comment | ${{ inputs.GERRIT_COMMENT }} |" - echo "| Namespace | ${{ inputs.FDIO_NAMESPACE }} |" - } >> "$GITHUB_STEP_SUMMARY" - - - name: Clear Gerrit vote - # yamllint disable-line rule:line-length - uses: lfreleng-actions/gerrit-review-action@a5de1d5bf17c2603ae81544dc2a455017b454ec8 # 1.1.1 - with: - host: ${{ vars.GERRIT_SERVER }} - username: ${{ vars.GERRIT_SSH_REQUIRED_USER }} - key: ${{ secrets.GERRIT_SSH_REQUIRED_PRIVKEY }} - known_hosts: ${{ vars.GERRIT_KNOWN_HOSTS }} - gerrit-change-number: ${{ inputs.GERRIT_CHANGE_NUMBER }} - gerrit-patchset-number: ${{ inputs.GERRIT_PATCHSET_NUMBER }} - vote-type: clear - comment-only: true - - verify-checkstyle: - name: Checkstyle - needs: - - start-verify - uses: fdio/vpp/.github/workflows/vpp-verify-checkstyle.yml@aea0a31a2b6bba3b9e47feccae473f4206f85704 - with: - FDIO_NAMESPACE: ${{ inputs.FDIO_NAMESPACE }} - GERRIT_BRANCH: ${{ inputs.GERRIT_BRANCH }} - GERRIT_CHANGE_ID: ${{ inputs.GERRIT_CHANGE_ID }} - GERRIT_CHANGE_NUMBER: ${{ inputs.GERRIT_CHANGE_NUMBER }} - GERRIT_CHANGE_URL: ${{ inputs.GERRIT_CHANGE_URL }} - GERRIT_EVENT_TYPE: ${{ inputs.GERRIT_EVENT_TYPE }} - GERRIT_PATCHSET_NUMBER: ${{ inputs.GERRIT_PATCHSET_NUMBER }} - GERRIT_PATCHSET_REVISION: ${{ inputs.GERRIT_PATCHSET_REVISION }} - GERRIT_PROJECT: ${{ inputs.GERRIT_PROJECT }} - GERRIT_REFSPEC: ${{ inputs.GERRIT_REFSPEC }} - LOG_PREFIX: ${{ needs.start-verify.outputs.log_prefix }} - secrets: inherit - - verify-maketest: - name: Maketest - needs: - - start-verify - - verify-checkstyle - if: ${{ needs.verify-checkstyle.result == 'success' }} - uses: fdio/vpp/.github/workflows/vpp-verify-maketest.yml@c316a780f8f25c5d2386ef8fa085e90338300e3a - with: - FDIO_NAMESPACE: ${{ inputs.FDIO_NAMESPACE }} - GERRIT_BRANCH: ${{ inputs.GERRIT_BRANCH }} - GERRIT_CHANGE_NUMBER: ${{ inputs.GERRIT_CHANGE_NUMBER }} - GERRIT_PATCHSET_NUMBER: ${{ inputs.GERRIT_PATCHSET_NUMBER }} - GERRIT_CHANGE_URL: ${{ inputs.GERRIT_CHANGE_URL }} - GERRIT_PROJECT: ${{ inputs.GERRIT_PROJECT }} - GERRIT_REFSPEC: ${{ inputs.GERRIT_REFSPEC }} - LOG_PREFIX: ${{ needs.start-verify.outputs.log_prefix }} - secrets: inherit - - verify-gcc: - name: GCC - needs: - - start-verify - - verify-checkstyle - if: ${{ needs.verify-checkstyle.result == 'success' }} - uses: fdio/vpp/.github/workflows/vpp-verify-gcc.yml@e5c8759e531c155d1aa983fcb0fe84fd1f004c88 - with: - FDIO_NAMESPACE: ${{ inputs.FDIO_NAMESPACE }} - GERRIT_BRANCH: ${{ inputs.GERRIT_BRANCH }} - GERRIT_CHANGE_ID: ${{ inputs.GERRIT_CHANGE_ID }} - GERRIT_CHANGE_NUMBER: ${{ inputs.GERRIT_CHANGE_NUMBER }} - GERRIT_CHANGE_URL: ${{ inputs.GERRIT_CHANGE_URL }} - GERRIT_EVENT_TYPE: ${{ inputs.GERRIT_EVENT_TYPE }} - GERRIT_PATCHSET_NUMBER: ${{ inputs.GERRIT_PATCHSET_NUMBER }} - GERRIT_PATCHSET_REVISION: ${{ inputs.GERRIT_PATCHSET_REVISION }} - GERRIT_PROJECT: ${{ inputs.GERRIT_PROJECT }} - GERRIT_REFSPEC: ${{ inputs.GERRIT_REFSPEC }} - LOG_PREFIX: ${{ needs.start-verify.outputs.log_prefix }} - secrets: inherit - - verify-csit-api: - name: CSIT API - needs: - - start-verify - - verify-checkstyle - if: ${{ needs.verify-checkstyle.result == 'success' }} - uses: fdio/vpp/.github/workflows/vpp-csit-verify-api.yml@e5c8759e531c155d1aa983fcb0fe84fd1f004c88 - with: - FDIO_NAMESPACE: ${{ inputs.FDIO_NAMESPACE }} - GERRIT_BRANCH: ${{ inputs.GERRIT_BRANCH }} - GERRIT_CHANGE_URL: ${{ inputs.GERRIT_CHANGE_URL }} - GERRIT_CHANGE_NUMBER: ${{ inputs.GERRIT_CHANGE_NUMBER }} - GERRIT_PATCHSET_NUMBER: ${{ inputs.GERRIT_PATCHSET_NUMBER }} - GERRIT_PROJECT: ${{ inputs.GERRIT_PROJECT }} - GERRIT_REFSPEC: ${{ inputs.GERRIT_REFSPEC }} - LOG_PREFIX: ${{ needs.start-verify.outputs.log_prefix }} - secrets: inherit - - verify-arm-drivers: - name: ARM Drivers - needs: - - start-verify - - verify-checkstyle - if: ${{ needs.verify-checkstyle.result == 'success' }} - uses: fdio/vpp/.github/workflows/vpp-verify-arm-drivers.yml@e5c8759e531c155d1aa983fcb0fe84fd1f004c88 - with: - FDIO_NAMESPACE: ${{ inputs.FDIO_NAMESPACE }} - GERRIT_BRANCH: ${{ inputs.GERRIT_BRANCH }} - GERRIT_CHANGE_NUMBER: ${{ inputs.GERRIT_CHANGE_NUMBER }} - GERRIT_PATCHSET_NUMBER: ${{ inputs.GERRIT_PATCHSET_NUMBER }} - GERRIT_CHANGE_URL: ${{ inputs.GERRIT_CHANGE_URL }} - GERRIT_PROJECT: ${{ inputs.GERRIT_PROJECT }} - GERRIT_REFSPEC: ${{ inputs.GERRIT_REFSPEC }} - LOG_PREFIX: ${{ needs.start-verify.outputs.log_prefix }} - secrets: inherit - - verify-hst: - name: HST - needs: - - start-verify - - verify-checkstyle - if: ${{ needs.verify-checkstyle.result == 'success' }} - uses: fdio/vpp/.github/workflows/vpp-verify-hst.yml@3dfe89762e12ccb3c20ce6b0b264da645e05e907 - with: - FDIO_NAMESPACE: ${{ inputs.FDIO_NAMESPACE }} - GERRIT_BRANCH: ${{ inputs.GERRIT_BRANCH }} - GERRIT_CHANGE_NUMBER: ${{ inputs.GERRIT_CHANGE_NUMBER }} - GERRIT_CHANGE_URL: ${{ inputs.GERRIT_CHANGE_URL }} - GERRIT_PATCHSET_NUMBER: ${{ inputs.GERRIT_PATCHSET_NUMBER }} - GERRIT_PROJECT: ${{ inputs.GERRIT_PROJECT }} - GERRIT_REFSPEC: ${{ inputs.GERRIT_REFSPEC }} - LOG_PREFIX: ${{ needs.start-verify.outputs.log_prefix }} - secrets: inherit - - detect-docs-changes: - name: Detect Docs Changes - needs: - - start-verify - - verify-checkstyle - runs-on: - - self-hosted - - nomad - - fdio:arch=x86_64 - - fdio:class=builder - - fdio:namespace=${{ inputs.FDIO_NAMESPACE || 'prod' }} - - fdio:os=ubuntu2404 - - fdio:size=csit - - fdio:workflow=${{ github.run_id }} - outputs: - docs_changed: ${{ steps.docs-changed.outputs.docs_changed }} - env: - WORKSPACE: /scratch/docker-build/vpp - GERRIT_BRANCH: ${{ inputs.GERRIT_BRANCH }} - GERRIT_REFSPEC: ${{ inputs.GERRIT_REFSPEC }} - TUI_LINE: "*******************************************************************" - steps: - - name: Setup Environment - id: setup-executor-env - continue-on-error: true - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/setup-executor-env@b728d1589228e2891a4241a1bba362d76895c225 - - - name: Checkout VPP Gerrit Change - id: checkout-vpp - continue-on-error: true - # yamllint disable-line rule:line-length - uses: fdio/vpp/.github/actions/vpp-checkout-gerrit-change@27fc2aa27f57c8f7055e8f1b6e5a9f41b0315873 - with: - WORKSPACE: ${{ env.WORKSPACE }} - GERRIT_BRANCH: ${{ env.GERRIT_BRANCH }} - GERRIT_REFSPEC: ${{ env.GERRIT_REFSPEC }} - TUI_LINE: ${{ env.TUI_LINE }} - - - name: Detect docs changes - id: docs-changed - if: ${{ always() }} - env: - CHECKOUT_OUTCOME: ${{ steps.checkout-vpp.outcome }} - run: | - set -euxo pipefail - - docs_changed="true" - if [[ "$CHECKOUT_OUTCOME" == "success" ]]; then - cd "$WORKSPACE" - if git diff-tree --no-commit-id --name-only -r --root HEAD | grep -Eq '(^docs/|\.rst$)'; then - docs_changed="true" - else - docs_changed="false" - fi - else - echo "warn: unable to checkout '${GERRIT_REFSPEC}'; defaulting docs_changed=true" >&2 - fi - - echo "docs_changed=$docs_changed" >> "$GITHUB_OUTPUT" - - verify-docs: - name: Docs - needs: - - start-verify - - detect-docs-changes - - verify-checkstyle - if: ${{ needs.verify-checkstyle.result == 'success' && needs.detect-docs-changes.outputs.docs_changed == 'true' }} - uses: fdio/vpp/.github/workflows/vpp-verify-docs.yml@09552c71554236d36c1eb51a1740a366019bc1a5 - with: - FDIO_NAMESPACE: ${{ inputs.FDIO_NAMESPACE }} - GERRIT_BRANCH: ${{ inputs.GERRIT_BRANCH }} - GERRIT_CHANGE_URL: ${{ inputs.GERRIT_CHANGE_URL }} - GERRIT_CHANGE_NUMBER: ${{ inputs.GERRIT_CHANGE_NUMBER }} - GERRIT_PATCHSET_NUMBER: ${{ inputs.GERRIT_PATCHSET_NUMBER }} - GERRIT_PROJECT: ${{ inputs.GERRIT_PROJECT }} - GERRIT_REFSPEC: ${{ inputs.GERRIT_REFSPEC }} - LOG_PREFIX: ${{ needs.start-verify.outputs.log_prefix }} - secrets: inherit - - final-vote: - name: Vote - if: ${{ always() }} - needs: - - start-verify - - detect-docs-changes - - verify-checkstyle - - verify-maketest - - verify-gcc - - verify-docs - - verify-csit-api - - verify-arm-drivers - - verify-hst - runs-on: ubuntu-latest - env: - GERRIT_CHANGE_NUMBER: ${{ inputs.GERRIT_CHANGE_NUMBER }} - GERRIT_PATCHSET_NUMBER: ${{ inputs.GERRIT_PATCHSET_NUMBER }} - steps: - - name: Collect workflow results - id: overall - run: | - set -euxo pipefail - docs_ok="true" - if [[ "${{ needs.detect-docs-changes.outputs.docs_changed }}" == "true" ]]; then - [[ "${{ needs.verify-docs.result }}" == "success" ]] || docs_ok="false" - fi - - if [[ "${{ needs.start-verify.result }}" == "success" && \ - "${{ needs.verify-checkstyle.result }}" == "success" && \ - "${{ needs.verify-maketest.result }}" == "success" && \ - "${{ needs.verify-gcc.result }}" == "success" && \ - "$docs_ok" == "true" && \ - "${{ needs.verify-csit-api.result }}" == "success" && \ - "${{ needs.verify-arm-drivers.result }}" == "success" && \ - "${{ needs.verify-hst.result }}" == "success" ]]; then - echo "result=success" >> "$GITHUB_OUTPUT" - else - echo "result=failure" >> "$GITHUB_OUTPUT" - fi - - - name: Gerrit Vote - # yamllint disable-line rule:line-length - uses: lfreleng-actions/gerrit-review-action@a5de1d5bf17c2603ae81544dc2a455017b454ec8 # 1.1.1 - with: - host: ${{ vars.GERRIT_SERVER }} - username: ${{ vars.GERRIT_SSH_USER }} - key: ${{ secrets.GERRIT_SSH_PRIVKEY }} - known_hosts: ${{ vars.GERRIT_KNOWN_HOSTS }} - gerrit-change-number: ${{ env.GERRIT_CHANGE_NUMBER }} - gerrit-patchset-number: ${{ env.GERRIT_PATCHSET_NUMBER }} - vote-type: ${{ steps.overall.outputs.result }} - comment-only: false # Vote on gerrit From 8eddcc30070f5df5723e0e9fcc3684d03b168430 Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 30 Jun 2026 00:58:18 +0800 Subject: [PATCH 05/85] github: fix build-deb workflow per review feedback - Trigger only on push to master (not fix/*) - Use sed pattern match instead of hardcoded line number - Use neutral version string for shallow clone fallback - Use head_commit.timestamp for build date - Add contents: write permission for release publishing --- .github/workflows/build-deb-trixie.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-deb-trixie.yml b/.github/workflows/build-deb-trixie.yml index ba01428cf35d..f3fff59eab3b 100644 --- a/.github/workflows/build-deb-trixie.yml +++ b/.github/workflows/build-deb-trixie.yml @@ -2,7 +2,7 @@ name: Build VPP DEB for Debian 13 (Trixie) on: push: - branches: [fix/*] + branches: [master] workflow_dispatch: inputs: release: @@ -17,6 +17,8 @@ on: jobs: build: runs-on: ubuntu-24.04 + permissions: + contents: write container: image: debian:trixie env: @@ -48,7 +50,7 @@ jobs: - name: Install VPP build dependencies run: | - sed -i '496s/./&-/' Makefile + sed -i 's/^\s*git config commit\.template/&-/' Makefile make UNATTENDED=yes install-dep - name: Save apt cache @@ -70,7 +72,7 @@ jobs: git config --global --add safe.directory "$PWD" git fetch --tags --force 2>/dev/null || true if ! git describe --tags --long --match 'v*' 2>/dev/null; then - echo "v26.10-0-g$(git rev-parse --short HEAD)" > src/scripts/.version + echo "0.0.0-0-g$(git rev-parse --short=7 HEAD)" > src/scripts/.version fi ccache -z export VPP_EXTRA_CMAKE_ARGS="\ @@ -101,6 +103,6 @@ jobs: body: | VPP build from commit ${{ github.sha }} - Branch: ${{ github.ref_name }} - - Build date: ${{ github.event.repository.updated_at }} + - Build date: ${{ github.event.head_commit.timestamp }} draft: false prerelease: true From 5dd71b8e34128af2cca18cb0d07dbcaca80e9f83 Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 30 Jun 2026 01:02:14 +0800 Subject: [PATCH 06/85] github: fix sed pattern and safe.directory ordering - Fix sed to prepend dash after tab (not append to matched text) - Add safe.directory before short SHA computation --- .github/workflows/build-deb-trixie.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-deb-trixie.yml b/.github/workflows/build-deb-trixie.yml index f3fff59eab3b..dc8721aace03 100644 --- a/.github/workflows/build-deb-trixie.yml +++ b/.github/workflows/build-deb-trixie.yml @@ -38,7 +38,9 @@ jobs: - name: Compute short SHA id: vars shell: bash - run: echo "short_sha=$(git rev-parse --short=7 HEAD)" >> $GITHUB_OUTPUT + run: | + git config --global --add safe.directory "$PWD" + echo "short_sha=$(git rev-parse --short=7 HEAD)" >> $GITHUB_OUTPUT - name: Restore apt cache uses: actions/cache/restore@v4 @@ -50,7 +52,7 @@ jobs: - name: Install VPP build dependencies run: | - sed -i 's/^\s*git config commit\.template/&-/' Makefile + sed -i '/^\s*git config commit\.template/s/^\(\s*\)/\1-/' Makefile make UNATTENDED=yes install-dep - name: Save apt cache From 0aca8e027394565b0b1901462cd3cc01853f7a67 Mon Sep 17 00:00:00 2001 From: sunoaki-ai-agent Date: Sat, 4 Jul 2026 05:45:09 +0000 Subject: [PATCH 07/85] vnet: fix runtime queue lifecycle hazards Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/vlib/handoff.c | 5 +++++ src/vnet/dev/handlers.c | 5 ----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/vlib/handoff.c b/src/vlib/handoff.c index 1ceb3fb96438..779a3df6a456 100644 --- a/src/vlib/handoff.c +++ b/src/vlib/handoff.c @@ -651,6 +651,8 @@ vlib_handoff_queue_resize (u32 index, u32 queue_size) hqm = vec_elt_at_index (vm->handoff_queue_mains, index); old_queue_size = hqm->size * VLIB_HANDOFF_QUEUE_SLOT_N_ELTS; + vlib_worker_thread_barrier_sync (vm); + vec_foreach_index (thread_index, hqm->vlib_handoff_queues) { vlib_handoff_queue_t *old_hq = hqm->vlib_handoff_queues[thread_index]; @@ -664,6 +666,7 @@ vlib_handoff_queue_resize (u32 index, u32 queue_size) vec_foreach (q, new_queues) clib_mem_free (q[0]); vec_free (new_queues); + vlib_worker_thread_barrier_release (vm); return clib_error_return (0, "queue has pending buffers using %u elements, " "size %u is too small", @@ -692,6 +695,8 @@ vlib_handoff_queue_resize (u32 index, u32 queue_size) vm->handoff_queue_mains = this_vm->handoff_queue_mains; } + vlib_worker_thread_barrier_release (this_vm); + return 0; } diff --git a/src/vnet/dev/handlers.c b/src/vnet/dev/handlers.c index 06f808d3e05e..58750499667c 100644 --- a/src/vnet/dev/handlers.c +++ b/src/vnet/dev/handlers.c @@ -306,11 +306,6 @@ vnet_dev_set_interface_next_node (vnet_main_t *vnm, u32 hw_if_index, } intf->redirect_to_node = 1; } - intf->rx_next_index = - node_index == ~0 ? - vnet_dev_default_next_index_by_port_type[di->port->attr.type] : - node_index; - if (runtime_update) { foreach_vnet_dev_port_rx_queue (rxq, di->port) From fe6c28d207d64844e75ad17fe0e10e99d687c505 Mon Sep 17 00:00:00 2001 From: sunoaki-ai-agent Date: Sat, 4 Jul 2026 05:46:38 +0000 Subject: [PATCH 08/85] http: harden request parsing and status reply Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/plugins/http/http1.c | 17 +++++++++-------- src/plugins/http_static/http_static.c | 3 ++- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/plugins/http/http1.c b/src/plugins/http/http1.c index 5ac98b438aa1..5d13d45dd1f1 100644 --- a/src/plugins/http/http1.c +++ b/src/plugins/http/http1.c @@ -255,7 +255,7 @@ static int http1_parse_request_line (http_ctx_t *hc, http_ctx_t *req, u8 *rx_buf, http_status_code_t *ec) { int i, target_len; - u32 next_line_offset, method_offset; + u32 next_line_offset, method_len, method_offset; /* request-line = method SP request-target SP HTTP-version CRLF */ i = http_v_find_index (rx_buf, 8, 0, "\r\n"); @@ -282,28 +282,29 @@ http1_parse_request_line (http_ctx_t *hc, http_ctx_t *req, u8 *rx_buf, http_stat * In the interest of robustness, a server that is expecting to receive and * parse a request-line SHOULD ignore at least one empty line (CRLF) * received prior to the request-line. - */ + */ method_offset = rx_buf[0] == '\r' && rx_buf[1] == '\n' ? 2 : 0; + method_len = i - method_offset; /* parse method */ - if (!memcmp (rx_buf + method_offset, "GET ", 4)) + if (method_len >= 4 && !memcmp (rx_buf + method_offset, "GET ", 4)) { HTTP_DBG (1, "GET method"); req->method = HTTP_REQ_GET; req->target_path_offset = method_offset + 4; } - else if (!memcmp (rx_buf + method_offset, "POST ", 5)) + else if (method_len >= 5 && !memcmp (rx_buf + method_offset, "POST ", 5)) { HTTP_DBG (1, "POST method"); req->method = HTTP_REQ_POST; req->target_path_offset = method_offset + 5; } - else if (!memcmp (rx_buf + method_offset, "PUT ", 4)) + else if (method_len >= 4 && !memcmp (rx_buf + method_offset, "PUT ", 4)) { HTTP_DBG (1, "PUT method"); req->method = HTTP_REQ_PUT; req->target_path_offset = method_offset + 4; } - else if (!memcmp (rx_buf + method_offset, "CONNECT ", 8)) + else if (method_len >= 8 && !memcmp (rx_buf + method_offset, "CONNECT ", 8)) { HTTP_DBG (1, "CONNECT method"); req->method = HTTP_REQ_CONNECT; @@ -311,7 +312,7 @@ http1_parse_request_line (http_ctx_t *hc, http_ctx_t *req, u8 *rx_buf, http_stat req->target_path_offset = method_offset + 8; req->req_flags |= HTTP_REQ_F_IS_TUNNEL; } - else if (!memcmp (rx_buf + method_offset, "CONNECT-UDP ", 12) && + else if (method_len >= 12 && !memcmp (rx_buf + method_offset, "CONNECT-UDP ", 12) && (hc->flags & HTTP_CONN_F_CONNECT_UDP_DRAFT03)) { HTTP_DBG (1, "CONNECT-UDP method"); @@ -321,7 +322,7 @@ http1_parse_request_line (http_ctx_t *hc, http_ctx_t *req, u8 *rx_buf, http_stat req->req_flags |= HTTP_REQ_F_IS_TUNNEL; req->req_flags |= HTTP_REQ_F_CONNECT_UDP_DRAFT03; } - else if (!memcmp (rx_buf + method_offset, "PRI ", 4)) + else if (method_len >= 4 && !memcmp (rx_buf + method_offset, "PRI ", 4)) { HTTP_DBG (1, "request start with PRI, upgrading to http/2"); return 2; diff --git a/src/plugins/http_static/http_static.c b/src/plugins/http_static/http_static.c index d014af654c7b..3cf0d472cc99 100644 --- a/src/plugins/http_static/http_static.c +++ b/src/plugins/http_static/http_static.c @@ -192,8 +192,9 @@ vl_api_http_static_get_t_handler (vl_api_http_static_get_t *mp) rmp->keepalive_timeout = htonl (hsm->default_listener.keepalive_timeout); rmp->max_body_size = htonl (hsm->default_listener.max_req_body_size); rmp->rx_buff_thresh = htonl (hsm->default_listener.rx_buff_thresh); - } + } }); + vec_free (addr); } #include From 85553839292d607bf58a2dff99d0e6ec49b4f527 Mon Sep 17 00:00:00 2001 From: sunoaki-ai-agent Date: Sat, 4 Jul 2026 05:47:49 +0000 Subject: [PATCH 09/85] svm: close memfd on init failures Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/svm/ssvm.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/svm/ssvm.c b/src/svm/ssvm.c index 41a7744e2b35..829cec54d1e0 100644 --- a/src/svm/ssvm.c +++ b/src/svm/ssvm.c @@ -235,6 +235,8 @@ ssvm_server_init_memfd (ssvm_private_t * memfd) if (log2_page_size == 0) { clib_unix_warning ("cannot determine page size"); + close (memfd->fd); + memfd->fd = -1; return SSVM_API_ERROR_CREATE_FAILURE; } @@ -243,6 +245,8 @@ ssvm_server_init_memfd (ssvm_private_t * memfd) if ((ftruncate (memfd->fd, n_pages << log2_page_size)) == -1) { clib_unix_warning ("memfd ftruncate failure"); + close (memfd->fd); + memfd->fd = -1; return SSVM_API_ERROR_CREATE_FAILURE; } @@ -253,6 +257,7 @@ ssvm_server_init_memfd (ssvm_private_t * memfd) { clib_unix_warning ("memfd map (fd %d)", memfd->fd); close (memfd->fd); + memfd->fd = -1; return SSVM_API_ERROR_CREATE_FAILURE; } From 9cd7199bbea5c4d08f1d5e7ae78cda9994b8776c Mon Sep 17 00:00:00 2001 From: sunoaki-ai-agent Date: Sat, 4 Jul 2026 05:49:02 +0000 Subject: [PATCH 10/85] tls: clean up failed DTLS connects Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/vnet/tls/tls.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/vnet/tls/tls.c b/src/vnet/tls/tls.c index d7df45a2b438..73e0ea9f6759 100644 --- a/src/vnet/tls/tls.c +++ b/src/vnet/tls/tls.c @@ -1309,7 +1309,12 @@ dtls_connect (transport_endpoint_cfg_t *tep) cargs->sep_ext.ns_index = app->ns_index; cargs->sep_ext.transport_flags = TRANSPORT_CFG_F_CONNECTED; if ((rv = vnet_connect (cargs))) - return rv; + { + tls_ctx_half_open_free (ctx_handle); + return rv; + } + + ctx->tls_session_handle = cargs->sh; TLS_DBG (1, "New DTLS connect request %x engine %d", ctx_handle, engine_type); From f8aeee2bc84a45989ca2ea314f62e01ce44046b8 Mon Sep 17 00:00:00 2001 From: sunoaki-ai-agent Date: Sat, 4 Jul 2026 05:50:25 +0000 Subject: [PATCH 11/85] vapi: bound socket message receive size Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/vpp-api/vapi/vapi.c | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/vpp-api/vapi/vapi.c b/src/vpp-api/vapi/vapi.c index 1fbe0e105568..5a999debf55c 100644 --- a/src/vpp-api/vapi/vapi.c +++ b/src/vpp-api/vapi/vapi.c @@ -55,6 +55,7 @@ typedef struct } vapi_req_t; static const u32 context_counter_mask = (1 << 31); +static const u32 vapi_sock_max_msg_size = 64 << 20; typedef struct { @@ -516,6 +517,12 @@ vapi_sock_recv_internal (vapi_ctx_t ctx, u8 **vec_msg, u32 timeout) mbp = (msgbuf_t *) (sock->rx_buffer); data_len = ntohl (mbp->data_len); + if (data_len > vapi_sock_max_msg_size || + data_len > UINT32_MAX - sizeof (*mbp)) + { + vec_reset_length (sock->rx_buffer); + return VAPI_EINVAL; + } current_rx_index = vec_len (sock->rx_buffer); vec_validate (sock->rx_buffer, current_rx_index + data_len); mbp = (msgbuf_t *) (sock->rx_buffer); @@ -753,7 +760,8 @@ vapi_sock_client_connect (vapi_ctx_t ctx, char *path, const char *name) if (vapi_sock_send (ctx, (void *) mp) != VAPI_OK) { - return VAPI_ECON_FAIL; + rv = VAPI_ECON_FAIL; + goto fail; } while (1) @@ -771,7 +779,10 @@ vapi_sock_client_connect (vapi_ctx_t ctx, char *path, const char *name) goto read_one_msg; if (qstatus != VAPI_EAGAIN) - return VAPI_ECON_FAIL; + { + rv = VAPI_ECON_FAIL; + goto fail; + } ts.tv_sec = 0; ts.tv_nsec = 10000 * 1000; /* 10 ms */ @@ -779,7 +790,8 @@ vapi_sock_client_connect (vapi_ctx_t ctx, char *path, const char *name) ts = tsrem; } /* Timeout... */ - return VAPI_ECON_FAIL; + rv = VAPI_ECON_FAIL; + goto fail; read_one_msg: if (vec_len (msg) == 0) @@ -796,6 +808,12 @@ vapi_sock_client_connect (vapi_ctx_t ctx, char *path, const char *name) break; } return (rv); + +fail: + clib_socket_close (sock); + clib_memset (sock, 0, sizeof (*sock)); + vec_free (msg); + return rv; } static void From ac6ca9e452c7ac9913b84721b06d2237c8d82e4c Mon Sep 17 00:00:00 2001 From: sunoaki-ai-agent Date: Sat, 4 Jul 2026 05:51:23 +0000 Subject: [PATCH 12/85] vhost: close received fds on errors Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/plugins/vhost/vhost_user.c | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/plugins/vhost/vhost_user.c b/src/plugins/vhost/vhost_user.c index 36637572496b..58de364a003e 100644 --- a/src/plugins/vhost/vhost_user.c +++ b/src/plugins/vhost/vhost_user.c @@ -398,6 +398,7 @@ vhost_user_socket_read (clib_file_t * uf) int n, i, j; int fd, number_of_fds = 0; int fds[VHOST_MEMORY_MAX_NREGIONS]; + u8 fd_is_owned[VHOST_MEMORY_MAX_NREGIONS] = {}; vhost_user_msg_t msg; struct msghdr mh; struct iovec iov[1]; @@ -578,6 +579,7 @@ vhost_user_socket_read (clib_file_t * uf) /* Do the mmap without barrier sync */ void *region_mmap_addr[VHOST_MEMORY_MAX_NREGIONS]; + ssize_t region_mmap_sz[VHOST_MEMORY_MAX_NREGIONS]; for (i = 0; i < msg.memory.nregions; i++) { long page_sz = get_huge_page_size (fds[i]); @@ -593,9 +595,10 @@ vhost_user_socket_read (clib_file_t * uf) { vu_log_err (vui, "failed to map memory. errno is %d", errno); for (j = 0; j < i; j++) - munmap (region_mmap_addr[j], map_sz); + munmap (region_mmap_addr[j], region_mmap_sz[j]); goto close_socket; } + region_mmap_sz[i] = map_sz; vu_log_debug (vui, "map memory region %d addr 0 len 0x%lx fd %d " "mapped 0x%lx page_sz 0x%x", i, map_sz, fds[i], region_mmap_addr[i], page_sz); @@ -615,6 +618,7 @@ vhost_user_socket_read (clib_file_t * uf) vui->region_mmap_addr[i] += vui->regions[i].mmap_offset; vui->region_mmap_fd[i] = fds[i]; + fd_is_owned[i] = 1; vui->nregions++; } @@ -791,6 +795,7 @@ vhost_user_socket_read (clib_file_t * uf) ((vui - vhost_user_main.vhost_user_interfaces) << 8) + q; template.description = format (0, "vhost user"); vui->vrings[q].callfd_idx = clib_file_add (&file_main, &template); + fd_is_owned[0] = 1; } else vui->vrings[q].callfd_idx = ~0; @@ -830,6 +835,7 @@ vhost_user_socket_read (clib_file_t * uf) (((uword) (vui - vhost_user_main.vhost_user_interfaces)) << 8) + q; vui->vrings[q].kickfd_idx = clib_file_add (&file_main, &template); + fd_is_owned[0] = 1; } else { @@ -862,6 +868,7 @@ vhost_user_socket_read (clib_file_t * uf) goto close_socket; vui->vrings[q].errfd = fds[0]; + fd_is_owned[0] = 1; } else vui->vrings[q].errfd = -1; @@ -1011,6 +1018,8 @@ vhost_user_socket_read (clib_file_t * uf) vui->log_base_addr = log_base_addr; vui->log_base_addr += msg.log.offset; vui->log_size = msg.log.size; + close (fd); + fds[0] = -1; vlib_worker_thread_barrier_release (vm); msg.flags |= 4; @@ -1090,6 +1099,9 @@ vhost_user_socket_read (clib_file_t * uf) return 0; close_socket: + for (i = 0; i < number_of_fds; i++) + if (!fd_is_owned[i] && fds[i] != -1) + close (fds[i]); vlib_worker_thread_barrier_sync (vm); vhost_user_if_disconnect (vui); vlib_worker_thread_barrier_release (vm); From a8eb93541ed1e952ddfca74b68d893eaeacb19ed Mon Sep 17 00:00:00 2001 From: sunoaki-ai-agent Date: Sat, 4 Jul 2026 05:52:38 +0000 Subject: [PATCH 13/85] snort: initialize empty buffer offsets safely Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/plugins/snort/snort.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/plugins/snort/snort.h b/src/plugins/snort/snort.h index 3ffdecbc1c50..584701f5653e 100644 --- a/src/plugins/snort/snort.h +++ b/src/plugins/snort/snort.h @@ -278,12 +278,12 @@ snort_qpair_empty_buf_alloc_buffers (vlib_main_t *vm, snort_qpair_t *qp) daq_vpp_empty_buf_desc_t *empty_buf_desc = &qp->empty_buf_ring[head & mask]; vlib_buffer_t *b = vlib_get_buffer (vm, qp->empty_buffers[head & mask]); + u8 buffer_pool = b->buffer_pool_index; b->current_data = 0; *empty_buf_desc = (daq_vpp_empty_buf_desc_t){ - .buffer_pool = b->buffer_pool_index, + .buffer_pool = buffer_pool, .length = vlib_buffer_get_default_data_size (vm), - .offset = (u8 *) b->data - - sm->buffer_pool_base_addrs[empty_buf_desc->buffer_pool], + .offset = (u8 *) b->data - sm->buffer_pool_base_addrs[buffer_pool], }; head++; i++; From 71af49f54cf86fc250b3b435942c8135c89c6243 Mon Sep 17 00:00:00 2001 From: sunoaki-ai-agent Date: Sat, 4 Jul 2026 05:53:59 +0000 Subject: [PATCH 14/85] iavf: initialize tx placeholder buffers Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/drivers/iavf/queue.c | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/src/drivers/iavf/queue.c b/src/drivers/iavf/queue.c index d008049717d8..f1b30b0cbb12 100644 --- a/src/drivers/iavf/queue.c +++ b/src/drivers/iavf/queue.c @@ -60,6 +60,9 @@ iavf_tx_queue_alloc (vlib_main_t *vm, vnet_dev_tx_queue_t *txq) vnet_dev_t *dev = txq->port->dev; iavf_device_t *ad = vnet_dev_get_data (dev); iavf_txq_t *atq = vnet_dev_get_tx_queue_data (txq); + u8 bpi = vlib_buffer_pool_get_default_for_numa (vm, dev->numa_node); + u16 n_ph_bufs = (txq->size / 510) + 1; + u32 n_alloc; vnet_dev_rv_t rv; if ((rv = @@ -68,6 +71,22 @@ iavf_tx_queue_alloc (vlib_main_t *vm, vnet_dev_tx_queue_t *txq) return rv; clib_ring_new_aligned (atq->rs_slots, 32, CLIB_CACHE_LINE_BYTES); + atq->ph_bufs = clib_mem_alloc_aligned (n_ph_bufs * sizeof (atq->ph_bufs[0]), + CLIB_CACHE_LINE_BYTES); + if (atq->ph_bufs == 0) + { + rv = VNET_DEV_ERR_BUFFER_ALLOC_FAIL; + goto error; + } + n_alloc = vlib_buffer_alloc_from_pool (vm, atq->ph_bufs, n_ph_bufs, bpi); + if (n_alloc != n_ph_bufs) + { + if (n_alloc) + vlib_buffer_free (vm, atq->ph_bufs, n_alloc); + rv = VNET_DEV_ERR_BUFFER_ALLOC_FAIL; + goto error; + } + atq->buffer_indices = clib_mem_alloc_aligned ( txq->size * sizeof (atq->buffer_indices[0]), CLIB_CACHE_LINE_BYTES); atq->tmp_descs = clib_mem_alloc_aligned ( @@ -79,6 +98,16 @@ iavf_tx_queue_alloc (vlib_main_t *vm, vnet_dev_tx_queue_t *txq) log_debug (dev, "queue %u alocated", txq->queue_id); return VNET_DEV_OK; + +error: + if (atq->ph_bufs) + { + clib_mem_free (atq->ph_bufs); + atq->ph_bufs = 0; + } + clib_ring_free (atq->rs_slots); + vnet_dev_dma_mem_free (vm, dev, atq->descs); + return rv; } void @@ -87,12 +116,16 @@ iavf_tx_queue_free (vlib_main_t *vm, vnet_dev_tx_queue_t *txq) vnet_dev_t *dev = txq->port->dev; iavf_txq_t *atq = vnet_dev_get_tx_queue_data (txq); iavf_txq_t *aq = vnet_dev_get_tx_queue_data (txq); + u16 n_ph_bufs = (txq->size / 510) + 1; log_debug (dev, "queue %u", txq->queue_id); vnet_dev_dma_mem_free (vm, dev, aq->descs); clib_ring_free (atq->rs_slots); - foreach_pointer (p, aq->tmp_descs, aq->tmp_bufs, aq->buffer_indices) + if (atq->ph_bufs) + vlib_buffer_free (vm, atq->ph_bufs, n_ph_bufs); + + foreach_pointer (p, aq->ph_bufs, aq->tmp_descs, aq->tmp_bufs, aq->buffer_indices) if (p) clib_mem_free (p); } From 16b632d58e55cc45aa23fb449454f51a3a07be21 Mon Sep 17 00:00:00 2001 From: sunoaki-ai-agent Date: Sat, 4 Jul 2026 06:03:29 +0000 Subject: [PATCH 15/85] iavf: fix placeholder buffer formatting Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/drivers/iavf/queue.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/drivers/iavf/queue.c b/src/drivers/iavf/queue.c index f1b30b0cbb12..a2feadaa46c4 100644 --- a/src/drivers/iavf/queue.c +++ b/src/drivers/iavf/queue.c @@ -71,8 +71,8 @@ iavf_tx_queue_alloc (vlib_main_t *vm, vnet_dev_tx_queue_t *txq) return rv; clib_ring_new_aligned (atq->rs_slots, 32, CLIB_CACHE_LINE_BYTES); - atq->ph_bufs = clib_mem_alloc_aligned (n_ph_bufs * sizeof (atq->ph_bufs[0]), - CLIB_CACHE_LINE_BYTES); + atq->ph_bufs = + clib_mem_alloc_aligned (n_ph_bufs * sizeof (atq->ph_bufs[0]), CLIB_CACHE_LINE_BYTES); if (atq->ph_bufs == 0) { rv = VNET_DEV_ERR_BUFFER_ALLOC_FAIL; From 2d25be1e98ee280b065ab248fb393cca21c3e1c3 Mon Sep 17 00:00:00 2001 From: sunoaki-ai-agent Date: Sat, 4 Jul 2026 08:34:30 +0000 Subject: [PATCH 16/85] http_static: fix disable cleanup edge cases Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/plugins/http_static/http_static.c | 21 +++++++++------------ src/plugins/http_static/static_server.c | 3 +++ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/plugins/http_static/http_static.c b/src/plugins/http_static/http_static.c index 3cf0d472cc99..4617290faf85 100644 --- a/src/plugins/http_static/http_static.c +++ b/src/plugins/http_static/http_static.c @@ -155,19 +155,11 @@ vl_api_http_static_get_t_handler (vl_api_http_static_get_t *mp) { hss_main_t *hsm = &hss_main; vl_api_http_static_get_reply_t *rmp; - u8 *addr = NULL; int rv = 0; - if (hsm->default_listener.sep.is_ip4) - { - addr = format (addr, "%U", format_ip4_address, &hsm->default_listener.sep.ip); - } - else - { - addr = format (addr, "[%U]", format_ip6_address, &hsm->default_listener.sep.ip); - } - REPLY_MACRO2 (VL_API_HTTP_STATIC_GET_REPLY, { + u8 *addr = 0; + if (pool_elts (hsm->listeners) == 0) { rmp->fifo_size = 0; @@ -181,6 +173,11 @@ vl_api_http_static_get_t_handler (vl_api_http_static_get_t *mp) } else { + if (hsm->default_listener.sep.is_ip4) + addr = format (addr, "%U", format_ip4_address, &hsm->default_listener.sep.ip); + else + addr = format (addr, "[%U]", format_ip6_address, &hsm->default_listener.sep.ip); + rmp->fifo_size = htonl (hsm->fifo_size); rmp->cache_size_limit = htonl (hsm->default_listener.cache.cache_limit); strncpy ((char *) rmp->www_root, (const char *) hsm->default_listener.www_root, @@ -192,9 +189,9 @@ vl_api_http_static_get_t_handler (vl_api_http_static_get_t *mp) rmp->keepalive_timeout = htonl (hsm->default_listener.keepalive_timeout); rmp->max_body_size = htonl (hsm->default_listener.max_req_body_size); rmp->rx_buff_thresh = htonl (hsm->default_listener.rx_buff_thresh); - } + vec_free (addr); + } }); - vec_free (addr); } #include diff --git a/src/plugins/http_static/static_server.c b/src/plugins/http_static/static_server.c index 82a0af66348f..b09a0b7caa96 100644 --- a/src/plugins/http_static/static_server.c +++ b/src/plugins/http_static/static_server.c @@ -1119,6 +1119,9 @@ hss_destroy (vlib_main_t *vm) hss_listener_t *l; int ret; + if (!hsm->is_init) + return 0; + while (pool_elts (hsm->listeners)) { l = pool_elt_at_index (hsm->listeners, pool_get_first_index (hsm->listeners)); From 12a727c41a8765e057d0169f1020253d4db70d2e Mon Sep 17 00:00:00 2001 From: sunoaki-ai-agent Date: Sat, 4 Jul 2026 08:35:22 +0000 Subject: [PATCH 17/85] tracepath: parse cli indexes as unsigned Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/plugins/tracepath/tracepath.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/plugins/tracepath/tracepath.c b/src/plugins/tracepath/tracepath.c index 8aed07c55035..555d28ba6a45 100644 --- a/src/plugins/tracepath/tracepath.c +++ b/src/plugins/tracepath/tracepath.c @@ -245,7 +245,7 @@ show_trace_paths_fn (vlib_main_t *vm, unformat_input_t *input, vlib_cli_command_ while (unformat_check_input (input) != (uword) UNFORMAT_END_OF_INPUT) { - if (unformat (input, "max %d", &max_paths)) + if (unformat (input, "max %u", &max_paths)) ; else return clib_error_create ("expected 'max COUNT', got `%U'", format_unformat_error, input); @@ -332,7 +332,7 @@ show_trace_paths_graphviz_fn (vlib_main_t *vm, unformat_input_t *input, vlib_cli while (unformat_check_input (input) != (uword) UNFORMAT_END_OF_INPUT) { - if (unformat (input, "max %d", &max_paths)) + if (unformat (input, "max %u", &max_paths)) ; else if (unformat (input, "file %U", unformat_vlib_tmpfile, &tmp_filename)) { @@ -340,7 +340,7 @@ show_trace_paths_graphviz_fn (vlib_main_t *vm, unformat_input_t *input, vlib_cli chroot_filename = tmp_filename; tmp_filename = 0; } - else if (unformat (input, "%d", &path_index)) + else if (unformat (input, "%u", &path_index)) hash_set (path_filter, path_index, 1); /* create path filter with user-requested paths */ else { @@ -490,9 +490,9 @@ show_trace_path_fn (vlib_main_t *vm, unformat_input_t *input, vlib_cli_command_t while (unformat_check_input (input) != (uword) UNFORMAT_END_OF_INPUT) { - if (unformat (input, "%d", &path_index)) + if (unformat (input, "%u", &path_index)) hash_set (path_filter, path_index, 1); - else if (unformat (input, "max %d", &max_traces)) + else if (unformat (input, "max %u", &max_traces)) ; else { From 1c1c6ef181ce4748ae7b45406e972f63e3ab2ce4 Mon Sep 17 00:00:00 2001 From: sunoaki-ai-agent Date: Sat, 4 Jul 2026 08:36:24 +0000 Subject: [PATCH 18/85] sfdp: print unsigned ids with unsigned formats Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/vnet/sfdp/sfdp.c | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/vnet/sfdp/sfdp.c b/src/vnet/sfdp/sfdp.c index 3383f64567ac..403e6e439c7a 100644 --- a/src/vnet/sfdp/sfdp.c +++ b/src/vnet/sfdp/sfdp.c @@ -293,8 +293,7 @@ sfdp_set_services (sfdp_main_t *sfdp, u32 tenant_id, sfdp_bitmap_t bitmap, clib_bihash_kv_8_8_t kv = { .key = tenant_id, .value = 0 }; sfdp_tenant_t *tenant; if (clib_bihash_search_inline_8_8 (&sfdp->tenant_idx_by_id, &kv)) - return clib_error_return ( - 0, "Can't assign service map: tenant id %d not found", tenant_id); + return clib_error_return (0, "Can't assign service map: tenant id %u not found", tenant_id); tenant = sfdp_tenant_at_index (sfdp, kv.value); tenant->bitmaps[direction] = bitmap; @@ -309,11 +308,10 @@ sfdp_set_timeout (sfdp_main_t *sfdp, u32 tenant_id, u32 timeout_idx, clib_bihash_kv_8_8_t kv = { .key = tenant_id, .value = 0 }; sfdp_tenant_t *tenant; if (timeout_idx >= SFDP_N_TIMEOUT) - return clib_error_return (0, "Can't configure timeout: timeout index %d outside valid range", + return clib_error_return (0, "Can't configure timeout: timeout index %u outside valid range", timeout_idx); if (clib_bihash_search_inline_8_8 (&sfdp->tenant_idx_by_id, &kv)) - return clib_error_return ( - 0, "Can't configure timeout: tenant id %d not found", tenant_id); + return clib_error_return (0, "Can't configure timeout: tenant id %u not found", tenant_id); tenant = sfdp_tenant_at_index (sfdp, kv.value); tenant->timeouts[timeout_idx] = timeout_val; return 0; @@ -327,8 +325,8 @@ sfdp_set_sp_node (sfdp_main_t *sfdp, u32 tenant_id, u32 sp_index, clib_bihash_kv_8_8_t kv = { .key = tenant_id, .value = 0 }; sfdp_tenant_t *tenant; if (clib_bihash_search_inline_8_8 (&sfdp->tenant_idx_by_id, &kv)) - return clib_error_return ( - 0, "Can't configure slow path node: tenant id %d not found", tenant_id); + return clib_error_return (0, "Can't configure slow path node: tenant id %u not found", + tenant_id); tenant = sfdp_tenant_at_index (sfdp, kv.value); tenant->sp_node_indices[sp_index] = node_index; return 0; @@ -344,8 +342,8 @@ sfdp_set_icmp_error_node (sfdp_main_t *sfdp, u32 tenant_id, u8 is_ip6, sfdp_tenant_t *tenant; uword next_index; if (clib_bihash_search_inline_8_8 (&sfdp->tenant_idx_by_id, &kv)) - return clib_error_return ( - 0, "Can't configure icmp error node: tenant id %d not found", tenant_id); + return clib_error_return (0, "Can't configure icmp error node: tenant id %u not found", + tenant_id); tenant = sfdp_tenant_at_index (sfdp, kv.value); if (is_ip6) { From 6c987a01f5ba4be28d218833c3aa2d4ae7c8f6b2 Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 21 Jul 2026 06:37:45 +0800 Subject: [PATCH 19/85] cnat: harden node lookup cleanup Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/plugins/cnat/cnat_node.h | 5 ++++- src/plugins/cnat/cnat_node_lookup.c | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/plugins/cnat/cnat_node.h b/src/plugins/cnat/cnat_node.h index 932997aa452b..0ce59660f4d4 100644 --- a/src/plugins/cnat/cnat_node.h +++ b/src/plugins/cnat/cnat_node.h @@ -926,6 +926,7 @@ cnat_rsession_create (cnat_timestamp_rewrite_t *rw, u32 flow_id, u32 ret_fib_ind { cnat_bihash_kv_t rkey = { 0 }; cnat_session_t *rsession = (cnat_session_t *) &rkey; + cnat_session_stale_cleanup_t cleanup = { 0 }; /* For ICMP echo, the echo identifier is a single field mapped to both * ports in the 5-tuple. Sync port[VLIB_TX] with port[VLIB_RX] (which @@ -993,9 +994,11 @@ cnat_rsession_create (cnat_timestamp_rewrite_t *rw, u32 flow_id, u32 ret_fib_ind (*sport_failures)++; } - cnat_bihash_add_with_overwrite_cb (&cnat_session_db, &rkey, cnat_session_free_stale_cb, NULL); + cnat_bihash_add_with_overwrite_cb (&cnat_session_db, &rkey, cnat_session_free_stale_cb, + &cleanup); out: + cnat_session_cleanup_stale (&cleanup); cnat_log_session_create (rsession); } diff --git a/src/plugins/cnat/cnat_node_lookup.c b/src/plugins/cnat/cnat_node_lookup.c index 5447cd2b0c01..635ba7bee4a2 100644 --- a/src/plugins/cnat/cnat_node_lookup.c +++ b/src/plugins/cnat/cnat_node_lookup.c @@ -88,6 +88,7 @@ cnat_writeback_new_flow (vlib_buffer_t *b, ip_address_family_t af, u16 *next) cnat_bihash_kv_t bkey; cnat_timestamp_t *ts; cnat_session_t *session = (cnat_session_t *) &bkey; + cnat_session_stale_cleanup_t cleanup = { 0 }; u32 n_retries = 0, rv, port_seed = 0; cnat_main_t *cm = &cnat_main; @@ -130,9 +131,11 @@ cnat_writeback_new_flow (vlib_buffer_t *b, ip_address_family_t af, u16 *next) vnet_buffer2 (b)->session.state = CNAT_LOOKUP_IS_DONE; retry_add_session: + cleanup.found = false; rv = cnat_bihash_add_with_overwrite_cb ( &cnat_session_db, &bkey, - n_retries < cm->session_max_port_retries ? NULL : cnat_session_free_stale_cb, NULL); + n_retries < cm->session_max_port_retries ? NULL : cnat_session_free_stale_cb, &cleanup); + cnat_session_cleanup_stale (&cleanup); if (rv && n_retries++ < cm->session_max_port_retries) { random_u32 (&port_seed); From bfb7dd58631e79c34487a15fdf7a054dc1c1c572 Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 21 Jul 2026 06:37:45 +0800 Subject: [PATCH 20/85] cnat: make session cleanup lifetime safe Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/plugins/cnat/cnat_session.c | 206 +++++++++++++++++++++----------- src/plugins/cnat/cnat_session.h | 11 ++ 2 files changed, 146 insertions(+), 71 deletions(-) diff --git a/src/plugins/cnat/cnat_session.c b/src/plugins/cnat/cnat_session.c index 87c483f19a55..aa4bbb32fab5 100644 --- a/src/plugins/cnat/cnat_session.c +++ b/src/plugins/cnat/cnat_session.c @@ -33,19 +33,17 @@ cnat_ep_trk_delete_notify (index_t *trk_index) void (*cnat_free_port_cb) (u32 fib_index, u16 port, ip_protocol_t iproto); -typedef struct cnat_session_walk_ctx_t_ +typedef struct cnat_session_snapshot_ctx_t_ { - cnat_session_walk_cb_t cb; - void *ctx; -} cnat_session_walk_ctx_t; + cnat_bihash_kv_t *sessions; +} cnat_session_snapshot_ctx_t; static int -cnat_session_walk_cb (BVT (clib_bihash_kv) * kv, void *arg) +cnat_session_snapshot_cb (BVT (clib_bihash_kv) * kv, void *arg) { - cnat_session_t *session = (cnat_session_t *) kv; - cnat_session_walk_ctx_t *ctx = arg; + cnat_session_snapshot_ctx_t *ctx = arg; - ctx->cb (session, ctx->ctx); + vec_add1 (ctx->sessions, *kv); return (BIHASH_WALK_CONTINUE); } @@ -53,27 +51,20 @@ cnat_session_walk_cb (BVT (clib_bihash_kv) * kv, void *arg) void cnat_session_walk (cnat_session_walk_cb_t cb, void *ctx) { - cnat_session_walk_ctx_t wctx = { - .cb = cb, - .ctx = ctx, - }; - BV (clib_bihash_foreach_key_value_pair) (&cnat_session_db, - cnat_session_walk_cb, &wctx); -} - -typedef struct cnat_session_purge_walk_t_ -{ - cnat_bihash_kv_t *keys; -} cnat_session_purge_walk_ctx_t; + cnat_session_snapshot_ctx_t snapshot = { 0 }; + cnat_bihash_kv_t *kv; + vlib_main_t *vm = vlib_get_main (); -static int -cnat_session_purge_walk (BVT (clib_bihash_kv) * key, void *arg) -{ - cnat_session_purge_walk_ctx_t *ctx = arg; + vlib_worker_thread_barrier_sync (vm); + BV (clib_bihash_foreach_key_value_pair) (&cnat_session_db, + cnat_session_snapshot_cb, &snapshot); + vlib_worker_thread_barrier_release (vm); - vec_add1 (ctx->keys, *key); + vec_foreach (kv, snapshot.sessions) + if (WALK_STOP == cb ((cnat_session_t *) kv, ctx)) + break; - return (BIHASH_WALK_CONTINUE); + vec_free (snapshot.sessions); } u8 * @@ -124,15 +115,12 @@ format_cnat_timestamp (u8 *s, va_list *args) return (s); } -u8 * -format_cnat_session (u8 * s, va_list * args) +static u8 * +format_cnat_session_data (u8 *s, const cnat_session_t *sess, const cnat_timestamp_t *ts, + int verbose) { - cnat_session_t *sess = va_arg (*args, cnat_session_t *); - int verbose = va_arg (*args, int); u32 indent = format_get_indent (s); - cnat_timestamp_t *ts = NULL; - ts = cnat_timestamp_get (sess->value.cs_session_index); s = format (s, "%U => [%U]\n%Uindex:%d fib:%d\n%U%U", format_cnat_5tuple, &sess->key.cs_5tuple, format_cnat_session_flags, sess->value.cs_flags, format_white_space, indent + 2, sess->value.cs_session_index, sess->key.fib_index, format_white_space, indent + 2, @@ -141,6 +129,16 @@ format_cnat_session (u8 * s, va_list * args) return (s); } +u8 * +format_cnat_session (u8 * s, va_list * args) +{ + cnat_session_t *sess = va_arg (*args, cnat_session_t *); + int verbose = va_arg (*args, int); + cnat_timestamp_t *ts = cnat_timestamp_get (sess->value.cs_session_index); + + return format_cnat_session_data (s, sess, ts, verbose); +} + static int cnat_show_yield (vlib_main_t *vm, f64 *start) { @@ -157,6 +155,17 @@ cnat_show_yield (vlib_main_t *vm, f64 *start) return 0; } +typedef struct +{ + cnat_session_t session; + cnat_timestamp_t timestamp; +} cnat_session_show_snapshot_t; + +typedef struct +{ + cnat_session_show_snapshot_t *sessions; +} cnat_session_show_snapshot_ctx_t; + typedef struct { vlib_main_t *vm; @@ -172,9 +181,31 @@ typedef struct } cnat_session_show_cbak_arg_t; static int -cnat_session_show_cbak (BVT (clib_bihash_kv) * kvp, void *arg) +cnat_session_show_snapshot_cb (BVT (clib_bihash_kv) * kvp, void *arg) +{ + cnat_session_show_snapshot_ctx_t *ctx = arg; + cnat_session_show_snapshot_t snapshot; + + snapshot.session = *(cnat_session_t *) kvp; + snapshot.timestamp = *cnat_timestamp_get (snapshot.session.value.cs_session_index); + vec_add1 (ctx->sessions, snapshot); + + return BIHASH_WALK_CONTINUE; +} + +static u8 * +format_cnat_session_show_snapshot (u8 *s, va_list *args) +{ + cnat_session_show_snapshot_t *snapshot = va_arg (*args, cnat_session_show_snapshot_t *); + int verbose = va_arg (*args, int); + + return format_cnat_session_data (s, &snapshot->session, &snapshot->timestamp, verbose); +} + +static int +cnat_session_show_snapshot_entry (cnat_session_show_snapshot_t *snapshot, void *arg) { - const cnat_session_t *s = (void *) kvp; + const cnat_session_t *s = &snapshot->session; cnat_session_show_cbak_arg_t *a = arg; cnat_show_yield (a->vm, &a->start); @@ -199,12 +230,11 @@ cnat_session_show_cbak (BVT (clib_bihash_kv) * kvp, void *arg) if (a->refcount) { - const cnat_timestamp_t *ts = cnat_timestamp_get (s->value.cs_session_index); - if (a->refcount != ts->ts_session_refcnt) + if (a->refcount != snapshot->timestamp.ts_session_refcnt) return BIHASH_WALK_CONTINUE; } - vlib_cli_output (a->vm, "%U\n", format_cnat_session, s, a->verbose); + vlib_cli_output (a->vm, "%U\n", format_cnat_session_show_snapshot, snapshot, a->verbose); if (a->max-- <= 0) { @@ -221,6 +251,8 @@ cnat_session_show (vlib_main_t * vm, unformat_input_t * input, vlib_cli_command_t * cmd) { cnat_session_show_cbak_arg_t arg = {}; + cnat_session_show_snapshot_ctx_t snapshot = { 0 }; + cnat_session_show_snapshot_t *session; int v; arg.vm = vm; @@ -257,11 +289,18 @@ cnat_session_show (vlib_main_t * vm, } } + vlib_worker_thread_barrier_sync (vm); vlib_cli_output (vm, "CNat Sessions: now:%f\n%U\n", vlib_time_now (vm), BV (format_bihash), &cnat_session_db, 0 /* verbose */); if (arg.verbose) - BV (clib_bihash_foreach_key_value_pair) - (&cnat_session_db, cnat_session_show_cbak, &arg); + BV (clib_bihash_foreach_key_value_pair) (&cnat_session_db, + cnat_session_show_snapshot_cb, &snapshot); + vlib_worker_thread_barrier_release (vm); + + vec_foreach (session, snapshot.sessions) + if (BIHASH_WALK_STOP == cnat_session_show_snapshot_entry (session, &arg)) + break; + vec_free (snapshot.sessions); return (NULL); } @@ -305,32 +344,56 @@ cnat_session_free__ (cnat_session_t *session) void cnat_session_free_stale_cb (cnat_bihash_kv_t *kv, void *opaque) { - cnat_session_t *session = (cnat_session_t *) kv; + cnat_session_stale_cleanup_t *cleanup = opaque; + + cleanup->session = *kv; + cleanup->found = true; +} + +void +cnat_session_cleanup_stale (cnat_session_stale_cleanup_t *cleanup) +{ + if (!cleanup->found) + return; + + cnat_session_t *session = (cnat_session_t *) &cleanup->session; cnat_log_session_overwrite (session); cnat_session_free__ (session); + cleanup->found = false; } void cnat_session_free (cnat_session_t * session) { - cnat_bihash_kv_t *bkey = (cnat_bihash_kv_t *) session; - cnat_session_free__ (session); - cnat_bihash_add_del (&cnat_session_db, bkey, 0 /* is_add */); + cnat_bihash_kv_t current; + cnat_session_t copy = *session; + + if (cnat_bihash_search_i2 (&cnat_session_db, (cnat_bihash_kv_t *) ©, ¤t) || + memcmp (©, ¤t, sizeof (copy))) + return; + + if (cnat_bihash_add_del (&cnat_session_db, (cnat_bihash_kv_t *) ©, 0 /* is_add */)) + return; + + cnat_session_free__ (©); } int cnat_session_purge (void) { /* flush all the session from the DB */ - cnat_session_purge_walk_ctx_t ctx = { }; + cnat_session_snapshot_ctx_t snapshot = { 0 }; cnat_bihash_kv_t *key; + vlib_main_t *vm = vlib_get_main (); + vlib_worker_thread_barrier_sync (vm); BV (clib_bihash_foreach_key_value_pair) (&cnat_session_db, - cnat_session_purge_walk, &ctx); + cnat_session_snapshot_cb, &snapshot); - vec_foreach (key, ctx.keys) cnat_session_free ((cnat_session_t *) key); + vec_foreach (key, snapshot.sessions) cnat_session_free ((cnat_session_t *) key); + vlib_worker_thread_barrier_release (vm); - vec_free (ctx.keys); + vec_free (snapshot.sessions); return (0); } @@ -436,6 +499,13 @@ cnat_session_is_stale (cnat_session_t *session, f64 start_time) u64 cnat_session_scan (vlib_main_t *vm, f64 start_time, int i) { + cnat_bihash_kv_t *stale = NULL, *key; + BVT (clib_bihash) * h = &cnat_session_db; + int j, k; + + cnat_log_scanner_start (i); + vlib_worker_thread_barrier_sync (vm); + if (!i) { /* delete confirmed deleted backends from previous scan @@ -455,19 +525,15 @@ cnat_session_scan (vlib_main_t *vm, f64 start_time, int i) */ ep_trk_del_state ^= 1; } - BVT (clib_bihash) * h = &cnat_session_db; - int j, k; - - cnat_log_scanner_start (i); if (!h->instantiated) - goto out; + goto remove_stale; for ( /* caller saves starting point */ ; i < h->nbuckets; i++) { /* allow no more than 100us without a pause */ if ((vlib_time_now (vm) - start_time) > 10e-5) - goto out; + goto remove_stale; if (i < (h->nbuckets - 3)) { @@ -496,29 +562,27 @@ cnat_session_scan (vlib_main_t *vm, f64 start_time, int i) cnat_session_t *session = (cnat_session_t *) & v->kvp[k]; if (cnat_session_is_stale (session, start_time)) - { - /* age it */ - cnat_log_session_expire (session); - cnat_reverse_session_free (session); - /* this should be last as deleting the session memset it to - * 0xff */ - cnat_session_free (session); - - /* - * Note: we may have just freed the bucket's backing - * storage, so check right here... - */ - if (BV (clib_bihash_bucket_is_empty) (b)) - goto doublebreak; - } + vec_add1 (stale, v->kvp[k]); } v++; } - doublebreak: - ; } -out: +remove_stale: + vec_foreach (key, stale) + { + cnat_bihash_kv_t current; + if (cnat_bihash_search_i2 (&cnat_session_db, key, ¤t) || + memcmp (key, ¤t, sizeof (*key))) + continue; + + cnat_session_t *session = (cnat_session_t *) ¤t; + cnat_log_session_expire (session); + cnat_reverse_session_free (session); + cnat_session_free (session); + } + vec_free (stale); + vlib_worker_thread_barrier_release (vm); cnat_log_scanner_stop (i); /* if at the end, return 0 to start again */ return i < h->nbuckets ? i : 0; diff --git a/src/plugins/cnat/cnat_session.h b/src/plugins/cnat/cnat_session.h index 5bbda9f2c2f7..592e506c8744 100644 --- a/src/plugins/cnat/cnat_session.h +++ b/src/plugins/cnat/cnat_session.h @@ -122,6 +122,12 @@ STATIC_ASSERT (sizeof (cnat_session_t) == sizeof (cnat_bihash_kv_t), */ extern cnat_bihash_t cnat_session_db; +typedef struct cnat_session_stale_cleanup_t_ +{ + cnat_bihash_kv_t session; + bool found; +} cnat_session_stale_cleanup_t; + /** * Callback function invoked during a walk of all sessions */ @@ -158,6 +164,11 @@ extern void cnat_session_free (cnat_session_t * session); */ extern void cnat_session_free_stale_cb (cnat_bihash_kv_t *kv, void *opaque); +/** + * Release resources recorded by cnat_session_free_stale_cb + */ +extern void cnat_session_cleanup_stale (cnat_session_stale_cleanup_t *cleanup); + /** * Port cleanup callback */ From 34e21189ed88d113203870525bb74e2202a291ed Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 21 Jul 2026 06:37:45 +0800 Subject: [PATCH 21/85] crypto: validate scheduler queue state Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/plugins/crypto_sw_scheduler/main.c | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/plugins/crypto_sw_scheduler/main.c b/src/plugins/crypto_sw_scheduler/main.c index da112b3df994..dc4205a279ba 100644 --- a/src/plugins/crypto_sw_scheduler/main.c +++ b/src/plugins/crypto_sw_scheduler/main.c @@ -575,11 +575,13 @@ crypto_sw_scheduler_config (vlib_main_t *vm, unformat_input_t *input) if (unformat (input, "crypto-sw-scheduler-queue-size %d", &crypto_sw_scheduler_queue_size)) { - if (!is_pow2 (crypto_sw_scheduler_queue_size)) + if (!crypto_sw_scheduler_queue_size || + !is_pow2 (crypto_sw_scheduler_queue_size) || + crypto_sw_scheduler_queue_size > VNET_CRYPTO_FRAME_POOL_SIZE) { - return clib_error_return (0, "input %d is not pow2", - format_unformat_error, - crypto_sw_scheduler_queue_size); + return clib_error_return ( + 0, "queue size must be a power of 2 between 1 and %u", + VNET_CRYPTO_FRAME_POOL_SIZE); } } else @@ -606,7 +608,7 @@ crypto_sw_scheduler_config (vlib_main_t *vm, unformat_input_t *input) vec_validate_aligned ( ptd->queue[CRYPTO_SW_SCHED_QUEUE_TYPE_DECRYPT].jobs, - CRYPTO_SW_SCHEDULER_QUEUE_SIZE - 1, CLIB_CACHE_LINE_BYTES); + crypto_sw_scheduler_queue_size - 1, CLIB_CACHE_LINE_BYTES); ptd->queue[CRYPTO_SW_SCHED_QUEUE_TYPE_ENCRYPT].head = 0; ptd->queue[CRYPTO_SW_SCHED_QUEUE_TYPE_ENCRYPT].tail = 0; @@ -616,7 +618,7 @@ crypto_sw_scheduler_config (vlib_main_t *vm, unformat_input_t *input) vec_validate_aligned ( ptd->queue[CRYPTO_SW_SCHED_QUEUE_TYPE_ENCRYPT].jobs, - CRYPTO_SW_SCHEDULER_QUEUE_SIZE - 1, CLIB_CACHE_LINE_BYTES); + crypto_sw_scheduler_queue_size - 1, CLIB_CACHE_LINE_BYTES); } if (error) From 8a3afc277db5c2fab9b061ac96133503c1198c30 Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 21 Jul 2026 06:37:45 +0800 Subject: [PATCH 22/85] dpdk: harden interrupt lifecycle Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/plugins/dpdk/device/common.c | 46 +++++++++++++++++++++++++++++++- src/plugins/dpdk/device/device.c | 2 ++ src/plugins/dpdk/device/dpdk.h | 1 + src/plugins/dpdk/device/init.c | 8 ++++++ 4 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/plugins/dpdk/device/common.c b/src/plugins/dpdk/device/common.c index 29bfdeaa8885..f70f5f7e8e67 100644 --- a/src/plugins/dpdk/device/common.c +++ b/src/plugins/dpdk/device/common.c @@ -252,6 +252,9 @@ dpdk_device_setup (dpdk_device_t *xd) CLIB_CACHE_LINE_BYTES); for (j = 0; j < xd->conf.n_tx_queues; j++) { + dpdk_tx_queue_t *txq = vec_elt_at_index (xd->tx_queues, j); + + clib_spinlock_free (&txq->lock); rv = rte_eth_tx_queue_setup (xd->port_id, j, xd->conf.n_tx_desc, xd->cpu_socket, 0); @@ -262,7 +265,7 @@ dpdk_device_setup (dpdk_device_t *xd) if (rv < 0) dpdk_device_error (xd, "rte_eth_tx_queue_setup", rv); - clib_spinlock_init (&vec_elt (xd->tx_queues, j).lock); + clib_spinlock_init (&txq->lock); } vec_validate_aligned (xd->rx_queues, xd->conf.n_rx_queues - 1, @@ -364,6 +367,34 @@ dpdk_rx_read_ready (clib_file_t *uf) return 0; } +static void +dpdk_remove_interrupt_files (dpdk_device_t *xd) +{ + clib_file_main_t *fm = &file_main; + + for (int q = 0; q < vec_len (xd->rx_queues); q++) + { + dpdk_rx_queue_t *rxq = vec_elt_at_index (xd->rx_queues, q); + clib_file_t *f; + + if (rxq->clib_file_index == (uword) ~0) + continue; + + f = clib_file_get (fm, rxq->clib_file_index); + if (f && !rxq->clib_file_registered) + { + fm->file_update (f, UNIX_FILE_UPDATE_ADD); + rxq->clib_file_registered = 1; + } + clib_file_del_by_index (fm, rxq->clib_file_index); + rxq->clib_file_index = ~0; + rxq->clib_file_registered = 0; + rxq->efd = -1; + } + + xd->flags &= ~DPDK_DEVICE_FLAG_INT_SUPPORTED; +} + static void dpdk_setup_interrupts (dpdk_device_t *xd) { @@ -373,6 +404,8 @@ dpdk_setup_interrupts (dpdk_device_t *xd) if (!hi) return; + dpdk_remove_interrupt_files (xd); + if (!xd->conf.enable_rxq_int) return; @@ -408,10 +441,12 @@ dpdk_setup_interrupts (dpdk_device_t *xd) f.read_function = dpdk_rx_read_ready; f.flags = UNIX_FILE_EVENT_EDGE_TRIGGERED; f.file_descriptor = rxq->efd; + f.dont_close = 1; f.private_data = rxq->queue_index; f.description = format (0, "%U queue %u", format_dpdk_device_name, xd->device_index, q); rxq->clib_file_index = clib_file_add (&file_main, &f); + rxq->clib_file_registered = 1; vnet_hw_if_set_rx_queue_file_index (vnm, rxq->queue_index, rxq->clib_file_index); if (xd->flags & DPDK_DEVICE_FLAG_INT_UNMASKABLE) @@ -419,10 +454,14 @@ dpdk_setup_interrupts (dpdk_device_t *xd) clib_file_main_t *fm = &file_main; clib_file_t *f = clib_file_get (fm, rxq->clib_file_index); fm->file_update (f, UNIX_FILE_UPDATE_DELETE); + rxq->clib_file_registered = 0; } } } + if (!int_mode) + dpdk_remove_interrupt_files (xd); + if (int_mode) vnet_hw_if_set_caps (vnm, hi->hw_if_index, VNET_HW_IF_CAP_INT_MODE); else @@ -474,9 +513,14 @@ dpdk_device_start (dpdk_device_t * xd) void dpdk_device_stop (dpdk_device_t * xd) { + vnet_main_t *vnm = vnet_get_main (); + if (xd->flags & DPDK_DEVICE_FLAG_PMD_INIT_FAIL) return; + dpdk_remove_interrupt_files (xd); + vnet_hw_if_unset_caps (vnm, xd->hw_if_index, VNET_HW_IF_CAP_INT_MODE); + vnet_hw_if_update_runtime_data (vnm, xd->hw_if_index); rte_eth_allmulticast_disable (xd->port_id); rte_eth_dev_stop (xd->port_id); clib_memset (&xd->link, 0, sizeof (struct rte_eth_link)); diff --git a/src/plugins/dpdk/device/device.c b/src/plugins/dpdk/device/device.c index de44614938ee..455948c0ed3e 100644 --- a/src/plugins/dpdk/device/device.c +++ b/src/plugins/dpdk/device/device.c @@ -738,6 +738,7 @@ dpdk_interface_rx_mode_change (vnet_main_t *vnm, u32 hw_if_index, u32 qid, rxq = vec_elt_at_index (xd->rx_queues, qid); f = clib_file_get (fm, rxq->clib_file_index); fm->file_update (f, UNIX_FILE_UPDATE_DELETE); + rxq->clib_file_registered = 0; } else if (!(xd->flags & DPDK_DEVICE_FLAG_INT_UNMASKABLE)) rv = rte_eth_dev_rx_intr_enable (xd->port_id, qid); @@ -746,6 +747,7 @@ dpdk_interface_rx_mode_change (vnet_main_t *vnm, u32 hw_if_index, u32 qid, rxq = vec_elt_at_index (xd->rx_queues, qid); f = clib_file_get (fm, rxq->clib_file_index); fm->file_update (f, UNIX_FILE_UPDATE_ADD); + rxq->clib_file_registered = 1; } if (rv) return clib_error_return (0, "dpdk_interface_rx_mode_change: %U", format_dpdk_rte_err, rv); diff --git a/src/plugins/dpdk/device/dpdk.h b/src/plugins/dpdk/device/dpdk.h index 7b0225e5e369..5eb1c09f0809 100644 --- a/src/plugins/dpdk/device/dpdk.h +++ b/src/plugins/dpdk/device/dpdk.h @@ -95,6 +95,7 @@ typedef struct u32 queue_index; int efd; uword clib_file_index; + u8 clib_file_registered; } dpdk_rx_queue_t; typedef struct diff --git a/src/plugins/dpdk/device/init.c b/src/plugins/dpdk/device/init.c index d96622573c2b..6a9313107a81 100644 --- a/src/plugins/dpdk/device/init.c +++ b/src/plugins/dpdk/device/init.c @@ -547,6 +547,14 @@ dpdk_lib_init (dpdk_main_t * dm) vec_validate_aligned (xd->tx_queues, xd->conf.n_tx_queues - 1, CLIB_CACHE_LINE_BYTES); + for (u32 q = 0; q < vec_len (xd->rx_queues); q++) + { + dpdk_rx_queue_t *rxq = vec_elt_at_index (xd->rx_queues, q); + rxq->efd = -1; + rxq->clib_file_index = ~0; + rxq->clib_file_registered = 0; + } + rte_eth_macaddr_get (port_id, (void *) addr); /* create interface */ From 84288c30e93fbaa32e604543221df179e57f7ff6 Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 21 Jul 2026 06:37:45 +0800 Subject: [PATCH 23/85] flowprobe: make disable lifecycle transactional Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/plugins/flowprobe/flowprobe.c | 365 +++++++++++++++++++++--------- src/plugins/flowprobe/flowprobe.h | 9 +- src/plugins/flowprobe/node.c | 62 +++-- 3 files changed, 310 insertions(+), 126 deletions(-) diff --git a/src/plugins/flowprobe/flowprobe.c b/src/plugins/flowprobe/flowprobe.c index e8651e9270bc..38da54022c43 100644 --- a/src/plugins/flowprobe/flowprobe.c +++ b/src/plugins/flowprobe/flowprobe.c @@ -428,6 +428,115 @@ flowprobe_template_add_del (u32 domain_id, u16 src_port, return vnet_flow_report_add_del (exp, &a, template_id); } +typedef struct +{ + flowprobe_record_t report_flags; + vnet_flow_data_callback_t *flow_data_callback; + vnet_flow_rewrite_callback_t *rewrite_callback; +} flowprobe_template_spec_t; + +static u32 +flowprobe_get_template_specs (flowprobe_variant_t which, flowprobe_record_t flags, + flowprobe_template_spec_t specs[3]) +{ + u32 n_specs = 0; + + if (which == FLOW_VARIANT_L2) + { + if (flags & FLOW_RECORD_L2) + specs[n_specs++] = (flowprobe_template_spec_t) { + .report_flags = flags, + .flow_data_callback = flowprobe_data_callback_l2, + .rewrite_callback = flowprobe_template_rewrite_l2, + }; + if (flags & FLOW_RECORD_L3 || flags & FLOW_RECORD_L4) + { + specs[n_specs++] = (flowprobe_template_spec_t) { + .report_flags = flags | FLOW_RECORD_L2_IP4, + .flow_data_callback = flowprobe_data_callback_l2, + .rewrite_callback = flowprobe_template_rewrite_l2_ip4, + }; + specs[n_specs++] = (flowprobe_template_spec_t) { + .report_flags = flags | FLOW_RECORD_L2_IP6, + .flow_data_callback = flowprobe_data_callback_l2, + .rewrite_callback = flowprobe_template_rewrite_l2_ip6, + }; + } + } + else if (which == FLOW_VARIANT_IP4) + specs[n_specs++] = (flowprobe_template_spec_t) { + .report_flags = flags, + .flow_data_callback = flowprobe_data_callback_ip4, + .rewrite_callback = flowprobe_template_rewrite_ip4, + }; + else if (which == FLOW_VARIANT_IP6) + specs[n_specs++] = (flowprobe_template_spec_t) { + .report_flags = flags, + .flow_data_callback = flowprobe_data_callback_ip6, + .rewrite_callback = flowprobe_template_rewrite_ip6, + }; + + return n_specs; +} + +static int +flowprobe_templates_add_del (flowprobe_main_t *fm, flowprobe_variant_t which, + flowprobe_record_t flags, bool is_add, bool *state_restored) +{ + flowprobe_template_spec_t specs[3]; + bool changed[3] = { 0 }; + u16 old_template_ids[3]; + u16 template_id; + u32 n_specs; + u32 i; + int rv; + + *state_restored = true; + n_specs = flowprobe_get_template_specs (which, flags, specs); + ASSERT (n_specs > 0); + + for (i = 0; i < n_specs; i++) + { + old_template_ids[i] = fm->template_reports[specs[i].report_flags]; + template_id = old_template_ids[i]; + rv = flowprobe_template_add_del (1, UDP_DST_PORT_ipfix, flags, specs[i].flow_data_callback, + specs[i].rewrite_callback, is_add, &template_id); + if (rv && !(is_add && rv == VNET_API_ERROR_VALUE_EXIST)) + goto rollback; + + changed[i] = rv == 0; + fm->template_reports[specs[i].report_flags] = is_add ? template_id : 0; + } + + return 0; + +rollback: + while (i > 0) + { + int rollback_rv; + + i--; + if (!changed[i]) + { + fm->template_reports[specs[i].report_flags] = old_template_ids[i]; + continue; + } + + template_id = old_template_ids[i]; + rollback_rv = + flowprobe_template_add_del (1, UDP_DST_PORT_ipfix, flags, specs[i].flow_data_callback, + specs[i].rewrite_callback, !is_add, &template_id); + if (rollback_rv && !(!is_add && rollback_rv == VNET_API_ERROR_VALUE_EXIST)) + { + *state_restored = false; + continue; + } + fm->template_reports[specs[i].report_flags] = is_add ? old_template_ids[i] : template_id; + } + + return rv; +} + static void flowprobe_expired_timer_callback (u32 * expired_timers) { @@ -532,6 +641,87 @@ flowprobe_clear_state_if_index (u32 sw_if_index) return error; } +static bool +flowprobe_wait_for_flush (vlib_main_t *vm, flowprobe_variant_t which, u64 generation) +{ + flowprobe_main_t *fm = &flowprobe_main; + f64 deadline = vlib_time_now (vm) + BARRIER_SYNC_TIMEOUT; + bool complete; + u32 i; + + vlib_worker_thread_barrier_release (vm); + do + { + complete = true; + for (i = 1; i < vlib_get_n_threads (); i++) + { + if (!vlib_get_main_by_index (i)) + continue; + if (clib_atomic_load_acq_n (&fm->flush_completed_generation_per_worker[which][i]) < + generation) + { + complete = false; + break; + } + } + if (!complete) + CLIB_PAUSE (); + } + while (!complete && vlib_time_now (vm) < deadline); + vlib_worker_thread_barrier_sync (vm); + + return complete; +} + +static u64 +flowprobe_flush_callback (flowprobe_variant_t which) +{ + if (which == FLOW_VARIANT_IP4) + return flowprobe_flush_callback_ip4 (); + if (which == FLOW_VARIANT_IP6) + return flowprobe_flush_callback_ip6 (); + + return flowprobe_flush_callback_l2 (); +} + +static void +flowprobe_feature_enable_disable (u32 sw_if_index, u8 which, u8 direction, int enable) +{ + if (direction == FLOW_DIRECTION_RX || direction == FLOW_DIRECTION_BOTH) + { + if (which == FLOW_VARIANT_IP4) + { + vnet_feature_enable_disable ("ip4-unicast", "flowprobe-input-ip4", sw_if_index, enable, 0, + 0); + vnet_feature_enable_disable ("ip4-multicast", "flowprobe-input-ip4", sw_if_index, enable, + 0, 0); + } + else if (which == FLOW_VARIANT_IP6) + { + vnet_feature_enable_disable ("ip6-unicast", "flowprobe-input-ip6", sw_if_index, enable, 0, + 0); + vnet_feature_enable_disable ("ip6-multicast", "flowprobe-input-ip6", sw_if_index, enable, + 0, 0); + } + else if (which == FLOW_VARIANT_L2) + vnet_feature_enable_disable ("device-input", "flowprobe-input-l2", sw_if_index, enable, 0, + 0); + } + + if (direction == FLOW_DIRECTION_TX || direction == FLOW_DIRECTION_BOTH) + { + if (which == FLOW_VARIANT_IP4) + vnet_feature_enable_disable ("ip4-output", "flowprobe-output-ip4", sw_if_index, enable, 0, + 0); + else if (which == FLOW_VARIANT_IP6) + vnet_feature_enable_disable ("ip6-output", "flowprobe-output-ip6", sw_if_index, enable, 0, + 0); + else if (which == FLOW_VARIANT_L2) + vnet_feature_enable_disable ("interface-output", "flowprobe-output-l2", sw_if_index, enable, + 0, 0); + } +} + static int validate_feature_on_interface (flowprobe_main_t * fm, u32 sw_if_index, u8 which) @@ -562,138 +752,92 @@ flowprobe_interface_add_del_feature (flowprobe_main_t *fm, u32 sw_if_index, u8 which, u8 direction, int is_add) { vlib_main_t *vm = vlib_get_main (); + bool template_state_restored = true; + bool update_template; int rv = 0; - u16 template_id = 0; flowprobe_record_t flags = fm->record; + u64 flush_generation; + + if (!is_add) + direction = fm->direction_per_interface[sw_if_index]; - fm->flow_per_interface[sw_if_index] = (is_add) ? which : (u8) ~ 0; - fm->direction_per_interface[sw_if_index] = (is_add) ? direction : (u8) ~0; - fm->template_per_flow[which] += (is_add) ? 1 : -1; - if (is_add && fm->template_per_flow[which] > 1) - template_id = fm->template_reports[flags]; + vlib_worker_thread_barrier_sync (vm); - if ((is_add && fm->template_per_flow[which] == 1) || - (!is_add && fm->template_per_flow[which] == 0)) + update_template = + (is_add && fm->template_per_flow[which] == 0) || (!is_add && fm->template_per_flow[which] == 1); + + if (!is_add) { - if (which == FLOW_VARIANT_L2) - { - if (!is_add) - { - flowprobe_flush_callback_l2 (); - } - if (fm->record & FLOW_RECORD_L2) - { - rv = flowprobe_template_add_del (1, UDP_DST_PORT_ipfix, flags, - flowprobe_data_callback_l2, - flowprobe_template_rewrite_l2, - is_add, &template_id); - fm->template_reports[flags] = (is_add) ? template_id : 0; - } - if (fm->record & FLOW_RECORD_L3 || fm->record & FLOW_RECORD_L4) - { - rv = flowprobe_template_add_del (1, UDP_DST_PORT_ipfix, flags, - flowprobe_data_callback_l2, - flowprobe_template_rewrite_l2_ip4, - is_add, &template_id); - fm->template_reports[flags | FLOW_RECORD_L2_IP4] = - (is_add) ? template_id : 0; - rv = - flowprobe_template_add_del (1, UDP_DST_PORT_ipfix, flags, - flowprobe_data_callback_l2, - flowprobe_template_rewrite_l2_ip6, - is_add, &template_id); - fm->template_reports[flags | FLOW_RECORD_L2_IP6] = - (is_add) ? template_id : 0; - - /* Special case L2 */ - fm->context[FLOW_VARIANT_L2_IP4].flags = - flags | FLOW_RECORD_L2_IP4; - fm->context[FLOW_VARIANT_L2_IP6].flags = - flags | FLOW_RECORD_L2_IP6; - } - } - else if (which == FLOW_VARIANT_IP4) - { - if (!is_add) - { - flowprobe_flush_callback_ip4 (); - } - rv = flowprobe_template_add_del ( - 1, UDP_DST_PORT_ipfix, flags, flowprobe_data_callback_ip4, - flowprobe_template_rewrite_ip4, is_add, &template_id); - fm->template_reports[flags] = (is_add) ? template_id : 0; - } - else if (which == FLOW_VARIANT_IP6) + flowprobe_feature_enable_disable (sw_if_index, which, direction, 0); + if (update_template) { - if (!is_add) + clib_atomic_store_rel_n (&fm->flush_in_progress, 1); + flush_generation = flowprobe_flush_callback (which); + if (!flowprobe_wait_for_flush (vm, which, flush_generation)) { - flowprobe_flush_callback_ip6 (); + clib_warning ("timed out waiting for flowprobe worker flush"); + clib_atomic_store_rel_n (&fm->flush_in_progress, 0); + flowprobe_feature_enable_disable (sw_if_index, which, direction, 1); + rv = VNET_API_ERROR_BUSY; + goto out; } - rv = flowprobe_template_add_del ( - 1, UDP_DST_PORT_ipfix, flags, flowprobe_data_callback_ip6, - flowprobe_template_rewrite_ip6, is_add, &template_id); - fm->template_reports[flags] = (is_add) ? template_id : 0; } } - if (rv && rv != VNET_API_ERROR_VALUE_EXIST) - { - clib_warning ("vnet_flow_report_add_del returned %d", rv); - return -1; - } - if (which != (u8) ~ 0) - { - fm->context[which].flags = fm->record; - } - - if (direction == FLOW_DIRECTION_RX || direction == FLOW_DIRECTION_BOTH) + if (update_template) + rv = flowprobe_templates_add_del (fm, which, flags, is_add, &template_state_restored); + if (rv) { - if (which == FLOW_VARIANT_IP4) - { - vnet_feature_enable_disable ("ip4-unicast", "flowprobe-input-ip4", - sw_if_index, is_add, 0, 0); - vnet_feature_enable_disable ("ip4-multicast", "flowprobe-input-ip4", - sw_if_index, is_add, 0, 0); - } - else if (which == FLOW_VARIANT_IP6) + clib_warning ("vnet_flow_report_add_del returned %d", rv); + if (!is_add) { - vnet_feature_enable_disable ("ip6-unicast", "flowprobe-input-ip6", - sw_if_index, is_add, 0, 0); - vnet_feature_enable_disable ("ip6-multicast", "flowprobe-input-ip6", - sw_if_index, is_add, 0, 0); + if (template_state_restored) + flowprobe_feature_enable_disable (sw_if_index, which, direction, 1); + else + { + fm->flow_per_interface[sw_if_index] = (u8) ~0; + fm->direction_per_interface[sw_if_index] = (u8) ~0; + fm->template_per_flow[which]--; + flowprobe_clear_state_if_index (sw_if_index); + } + clib_atomic_store_rel_n (&fm->flush_in_progress, 0); } - else if (which == FLOW_VARIANT_L2) - vnet_feature_enable_disable ("device-input", "flowprobe-input-l2", - sw_if_index, is_add, 0, 0); + rv = -1; + goto out; } - if (direction == FLOW_DIRECTION_TX || direction == FLOW_DIRECTION_BOTH) + fm->flow_per_interface[sw_if_index] = is_add ? which : (u8) ~0; + fm->direction_per_interface[sw_if_index] = is_add ? direction : (u8) ~0; + fm->template_per_flow[which] += is_add ? 1 : -1; + fm->context[which].flags = fm->record; + if (which == FLOW_VARIANT_L2 && (flags & FLOW_RECORD_L3 || flags & FLOW_RECORD_L4)) { - if (which == FLOW_VARIANT_IP4) - vnet_feature_enable_disable ("ip4-output", "flowprobe-output-ip4", - sw_if_index, is_add, 0, 0); - else if (which == FLOW_VARIANT_IP6) - vnet_feature_enable_disable ("ip6-output", "flowprobe-output-ip6", - sw_if_index, is_add, 0, 0); - else if (which == FLOW_VARIANT_L2) - vnet_feature_enable_disable ("interface-output", "flowprobe-output-l2", - sw_if_index, is_add, 0, 0); + fm->context[FLOW_VARIANT_L2_IP4].flags = flags | FLOW_RECORD_L2_IP4; + fm->context[FLOW_VARIANT_L2_IP6].flags = flags | FLOW_RECORD_L2_IP6; } - /* Stateful flow collection */ - if (is_add && !fm->initialized) + if (is_add) { - flowprobe_create_state_tables (fm->active_timer); - if (fm->active_timer) - vlib_process_signal_event (vm, flowprobe_timer_node.index, 1, 0); + if (!fm->initialized || (fm->active_timer && !fm->hash_per_worker)) + { + flowprobe_create_state_tables (fm->active_timer); + if (fm->active_timer) + vlib_process_signal_event (vm, flowprobe_timer_node.index, 1, 0); + } + flowprobe_feature_enable_disable (sw_if_index, which, direction, 1); } - - if (!is_add && fm->initialized) + else { flowprobe_clear_state_if_index (sw_if_index); + if (update_template) + clib_atomic_store_rel_n (&fm->flush_in_progress, 0); } - return 0; + rv = 0; + +out: + vlib_worker_thread_barrier_release (vm); + return rv; } /** @@ -718,7 +862,7 @@ void vl_api_flowprobe_tx_interface_add_del_t_handler } rv = validate_feature_on_interface (fm, sw_if_index, mp->which); - if ((rv == 1 && mp->is_add == 1) || rv == 0) + if ((rv == 1 && mp->is_add == 1) || rv == 0 || (rv == -1 && mp->is_add == 0)) { rv = VNET_API_ERROR_CANNOT_ENABLE_DISABLE_FEATURE; goto out; @@ -1312,6 +1456,7 @@ VLIB_CLI_COMMAND (flowprobe_enable_disable_command, static) = { .short_help = "flowprobe feature add-del [(l2|ip4|ip6)] " "[(rx|tx|both)] [disable]", .function = flowprobe_interface_add_del_feature_command_fn, + .is_mp_safe = 1, }; VLIB_CLI_COMMAND (flowprobe_params_command, static) = { .path = "flowprobe params", @@ -1413,6 +1558,7 @@ flowprobe_init (vlib_main_t * vm) { flowprobe_main_t *fm = &flowprobe_main; vlib_thread_main_t *tm = &vlib_thread_main; + api_main_t *am = vlibapi_get_main (); clib_error_t *error = 0; u32 num_threads; int i; @@ -1421,6 +1567,8 @@ flowprobe_init (vlib_main_t * vm) /* Ask for a correctly-sized block of API message decode slots */ fm->msg_id_base = setup_message_id_table (); + vl_api_set_msg_thread_safe (am, fm->msg_id_base + VL_API_FLOWPROBE_TX_INTERFACE_ADD_DEL, 1); + vl_api_set_msg_thread_safe (am, fm->msg_id_base + VL_API_FLOWPROBE_INTERFACE_ADD_DEL, 1); /* Set up time reference pair */ fm->vlib_time_0 = vlib_time_now (vm); @@ -1436,6 +1584,7 @@ flowprobe_init (vlib_main_t * vm) /* Allocate per worker thread vectors per flavour */ for (i = 0; i < FLOW_N_VARIANTS; i++) { + vec_validate (fm->flush_completed_generation_per_worker[i], num_threads - 1); vec_validate (fm->context[i].buffers_per_worker, num_threads - 1); vec_validate (fm->context[i].frames_per_worker, num_threads - 1); vec_validate (fm->context[i].next_record_offset_per_worker, diff --git a/src/plugins/flowprobe/flowprobe.h b/src/plugins/flowprobe/flowprobe.h index abe9486f90db..8fbca0d9ac39 100644 --- a/src/plugins/flowprobe/flowprobe.h +++ b/src/plugins/flowprobe/flowprobe.h @@ -130,6 +130,8 @@ typedef struct flowprobe_entry_t **pool_per_worker; TWT (tw_timer_wheel) ** timers_per_worker; u32 **expired_passive_per_worker; + u64 *flush_completed_generation_per_worker[FLOW_N_VARIANTS]; + u64 flush_generation[FLOW_N_VARIANTS]; flowprobe_record_t record; u32 active_timer; @@ -138,6 +140,7 @@ typedef struct bool initialized; bool disabled; + u32 flush_in_progress; u16 template_per_flow[FLOW_N_VARIANTS]; u8 *flow_per_interface; @@ -154,9 +157,9 @@ extern vlib_node_registration_t flowprobe_walker_node; void flowprobe_delete_by_index (u32 my_cpu_number, u32 poolindex); -void flowprobe_flush_callback_ip4 (void); -void flowprobe_flush_callback_ip6 (void); -void flowprobe_flush_callback_l2 (void); +u64 flowprobe_flush_callback_ip4 (void); +u64 flowprobe_flush_callback_ip6 (void); +u64 flowprobe_flush_callback_l2 (void); u8 *format_flowprobe_entry (u8 * s, va_list * args); #endif diff --git a/src/plugins/flowprobe/node.c b/src/plugins/flowprobe/node.c index 6faa428eb84e..b2bb56311375 100644 --- a/src/plugins/flowprobe/node.c +++ b/src/plugins/flowprobe/node.c @@ -925,18 +925,37 @@ flowprobe_output_l2_node_fn (vlib_main_t *vm, vlib_node_runtime_t *node, } static inline void -flush_record (flowprobe_variant_t which) +flush_record (vlib_main_t *vm, flowprobe_variant_t which) { - vlib_main_t *vm = vlib_get_main (); vlib_buffer_t *b = flowprobe_get_buffer (vm, which); if (b) flowprobe_export_send (vm, b, which); } -void +static u64 +flowprobe_flush_generation_next (flowprobe_variant_t which) +{ + flowprobe_main_t *fm = &flowprobe_main; + + return clib_atomic_add_fetch (&fm->flush_generation[which], 1); +} + +static void +flowprobe_flush_complete (vlib_main_t *vm, flowprobe_variant_t which) +{ + flowprobe_main_t *fm = &flowprobe_main; + u64 generation = clib_atomic_load_acq_n (&fm->flush_generation[which]); + + clib_atomic_store_rel_n (&fm->flush_completed_generation_per_worker[which][vm->thread_index], + generation); +} + +u64 flowprobe_flush_callback_ip4 (void) { + vlib_main_t *vm = vlib_get_main (); vlib_main_t *worker_vm; + u64 generation = flowprobe_flush_generation_next (FLOW_VARIANT_IP4); u32 i; /* Flush for each worker thread */ @@ -949,13 +968,16 @@ flowprobe_flush_callback_ip4 (void) } /* Flush for the main thread */ - flush_record (FLOW_VARIANT_IP4); + flush_record (vm, FLOW_VARIANT_IP4); + return generation; } -void +u64 flowprobe_flush_callback_ip6 (void) { + vlib_main_t *vm = vlib_get_main (); vlib_main_t *worker_vm; + u64 generation = flowprobe_flush_generation_next (FLOW_VARIANT_IP6); u32 i; /* Flush for each worker thread */ @@ -968,13 +990,16 @@ flowprobe_flush_callback_ip6 (void) } /* Flush for the main thread */ - flush_record (FLOW_VARIANT_IP6); + flush_record (vm, FLOW_VARIANT_IP6); + return generation; } -void +u64 flowprobe_flush_callback_l2 (void) { + vlib_main_t *vm = vlib_get_main (); vlib_main_t *worker_vm; + u64 generation = flowprobe_flush_generation_next (FLOW_VARIANT_L2); u32 i; /* Flush for each worker thread */ @@ -987,9 +1012,10 @@ flowprobe_flush_callback_l2 (void) } /* Flush for the main thread */ - flush_record (FLOW_VARIANT_L2); - flush_record (FLOW_VARIANT_L2_IP4); - flush_record (FLOW_VARIANT_L2_IP6); + flush_record (vm, FLOW_VARIANT_L2); + flush_record (vm, FLOW_VARIANT_L2_IP4); + flush_record (vm, FLOW_VARIANT_L2_IP6); + return generation; } void @@ -1020,6 +1046,9 @@ flowprobe_walker_process (vlib_main_t * vm, flowprobe_entry_t *e; ipfix_exporter_t *exp = pool_elt_at_index (flow_report_main.exporters, 0); + if (clib_atomic_load_acq_n (&fm->flush_in_progress)) + return 0; + /* * $$$$ Remove this check from here and track FRM status and disable * this process if required. @@ -1095,7 +1124,8 @@ flowprobe_walker_process (vlib_main_t * vm, static uword flowprobe_flush_ip4 (vlib_main_t *vm, vlib_node_runtime_t *rt, vlib_frame_t *f) { - flush_record (FLOW_VARIANT_IP4); + flush_record (vm, FLOW_VARIANT_IP4); + flowprobe_flush_complete (vm, FLOW_VARIANT_IP4); return 0; } @@ -1103,7 +1133,8 @@ flowprobe_flush_ip4 (vlib_main_t *vm, vlib_node_runtime_t *rt, vlib_frame_t *f) static uword flowprobe_flush_ip6 (vlib_main_t *vm, vlib_node_runtime_t *rt, vlib_frame_t *f) { - flush_record (FLOW_VARIANT_IP6); + flush_record (vm, FLOW_VARIANT_IP6); + flowprobe_flush_complete (vm, FLOW_VARIANT_IP6); return 0; } @@ -1111,9 +1142,10 @@ flowprobe_flush_ip6 (vlib_main_t *vm, vlib_node_runtime_t *rt, vlib_frame_t *f) static uword flowprobe_flush_l2 (vlib_main_t *vm, vlib_node_runtime_t *rt, vlib_frame_t *f) { - flush_record (FLOW_VARIANT_L2); - flush_record (FLOW_VARIANT_L2_IP4); - flush_record (FLOW_VARIANT_L2_IP6); + flush_record (vm, FLOW_VARIANT_L2); + flush_record (vm, FLOW_VARIANT_L2_IP4); + flush_record (vm, FLOW_VARIANT_L2_IP6); + flowprobe_flush_complete (vm, FLOW_VARIANT_L2); return 0; } From 0ff4a565f40f5968589880861260a95549688265 Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 21 Jul 2026 06:37:45 +0800 Subject: [PATCH 24/85] memif: validate zero-copy descriptors Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/plugins/memif/device.c | 12 ++++++++++-- src/plugins/memif/node.c | 40 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/src/plugins/memif/device.c b/src/plugins/memif/device.c index 367728ec8f40..54e6a2848d0b 100644 --- a/src/plugins/memif/device.c +++ b/src/plugins/memif/device.c @@ -17,7 +17,8 @@ #define foreach_memif_tx_func_error \ _ (NO_FREE_SLOTS, no_free_slots, ERROR, "no free tx slots") \ - _ (ROLLBACK, rollback, ERROR, "no enough space in tx buffers") + _ (ROLLBACK, rollback, ERROR, "no enough space in tx buffers") \ + _ (BAD_RING, bad_ring, ERROR, "invalid ring metadata") typedef enum { @@ -308,7 +309,7 @@ memif_interface_tx_zc_inline (vlib_main_t *vm, vlib_node_runtime_t *node, memif_per_thread_data_t *ptd, u32 n_left) { memif_ring_t *ring = mq->ring; - u16 slot, free_slots, n_free; + u16 slot, free_slots, n_free, n_used; u16 ring_size = 1 << mq->log2_ring_size; u16 mask = ring_size - 1; int n_retries = 5; @@ -322,6 +323,13 @@ memif_interface_tx_zc_inline (vlib_main_t *vm, vlib_node_runtime_t *node, slot = head = ring->head; n_free = tail - mq->last_tail; + n_used = head - mq->last_tail; + if (PREDICT_FALSE (n_used > ring_size || n_free > n_used)) + { + vlib_error_count (vm, node->node_index, MEMIF_TX_ERROR_BAD_RING, 1); + return n_left; + } + if (n_free >= 16) { vlib_buffer_free_from_ring_no_next (vm, mq->buffers, diff --git a/src/plugins/memif/node.c b/src/plugins/memif/node.c index 41bdc65e92e3..58096ddfefc2 100644 --- a/src/plugins/memif/node.c +++ b/src/plugins/memif/node.c @@ -220,6 +220,29 @@ memif_validate_desc_data (memif_per_thread_data_t *ptd, memif_if_t *mif, ptd->n_rx_bytes = n_rx_bytes; } +static_always_inline int +memif_zc_rx_packet_valid (memif_ring_t *ring, u16 cur_slot, u16 n_slots, + u16 mask, u32 buffer_length, u16 *packet_slots) +{ + int valid = 1; + + *packet_slots = 0; + while (*packet_slots < n_slots) + { + memif_desc_t *d = &ring->desc[(cur_slot + *packet_slots) & mask]; + + (*packet_slots)++; + if (d->length == 0 || d->length > buffer_length || + (d->flags & ~MEMIF_DESC_FLAG_NEXT)) + valid = 0; + + if ((d->flags & MEMIF_DESC_FLAG_NEXT) == 0) + return valid; + } + + return 0; +} + static_always_inline u32 memif_process_desc (vlib_main_t *vm, vlib_node_runtime_t *node, memif_per_thread_data_t *ptd, memif_if_t *mif) @@ -784,12 +807,29 @@ memif_device_input_zc_inline (vlib_main_t *vm, vlib_node_runtime_t *node, goto refill; n_slots = last_slot - cur_slot; + if (PREDICT_FALSE (n_slots > ring_size)) + { + vlib_error_count (vm, node->node_index, MEMIF_INPUT_ERROR_BAD_DESC, 1); + goto done; + } + /* process ring slots */ vec_validate_aligned (ptd->buffers, MEMIF_RX_VECTOR_SZ, CLIB_CACHE_LINE_BYTES); while (n_slots && n_rx_packets < MEMIF_RX_VECTOR_SZ) { vlib_buffer_t *hb; + u16 packet_slots; + + if (PREDICT_FALSE (!memif_zc_rx_packet_valid ( + ring, cur_slot, n_slots, mask, buffer_length, &packet_slots))) + { + cur_slot += packet_slots; + n_slots -= packet_slots; + vlib_error_count (vm, node->node_index, MEMIF_INPUT_ERROR_BAD_DESC, + 1); + continue; + } s0 = cur_slot & mask; bi0 = mq->buffers[s0]; From 09cbb5eaf953fb735f5f0f1a16f04a912e33e6ea Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 21 Jul 2026 06:38:04 +0800 Subject: [PATCH 25/85] nat: widen worker index handling Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/plugins/nat/nat44-ed/nat44_ed.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/plugins/nat/nat44-ed/nat44_ed.c b/src/plugins/nat/nat44-ed/nat44_ed.c index 3c3c51fea3a4..1d1f27051512 100644 --- a/src/plugins/nat/nat44-ed/nat44_ed.c +++ b/src/plugins/nat/nat44-ed/nat44_ed.c @@ -3082,7 +3082,8 @@ nat44_ed_get_out2in_worker_index (vlib_buffer_t *b, ip4_header_t *ip, snat_main_t *sm = &snat_main; clib_bihash_kv_16_8_t kv16, value16; - u8 proto, next_worker_index = 0; + u8 proto; + u32 next_worker_index = 0; u16 port; snat_static_mapping_t *m; u32 hash; From faa9de939777b1a5d7998c4e6f31382228ab1524 Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 21 Jul 2026 06:38:04 +0800 Subject: [PATCH 26/85] rdma: close resources on failure Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/plugins/rdma/cli.c | 24 +++++++++---- src/plugins/rdma/device.c | 76 +++++++++++++++++++++++++++++++++------ src/plugins/rdma/input.c | 18 ++++++---- 3 files changed, 95 insertions(+), 23 deletions(-) diff --git a/src/plugins/rdma/cli.c b/src/plugins/rdma/cli.c index 2e6341582980..a1fb277b0102 100644 --- a/src/plugins/rdma/cli.c +++ b/src/plugins/rdma/cli.c @@ -21,8 +21,12 @@ rdma_create_command_fn (vlib_main_t * vm, unformat_input_t * input, rdma_create_if_args_t args; if (!unformat_user (input, unformat_rdma_create_if_args, &args)) - return clib_error_return (0, "unknown input `%U'", - format_unformat_error, input); + { + vec_free (args.ifname); + vec_free (args.name); + return clib_error_return (0, "unknown input `%U'", + format_unformat_error, input); + } rdma_create_if (vm, &args); @@ -65,8 +69,12 @@ rdma_delete_command_fn (vlib_main_t * vm, unformat_input_t * input, vnm, &sw_if_index)) ; else - return clib_error_return (0, "unknown input `%U'", - format_unformat_error, input); + { + clib_error_t *error = clib_error_return ( + 0, "unknown input `%U'", format_unformat_error, input); + unformat_free (line_input); + return error; + } } unformat_free (line_input); @@ -116,8 +124,12 @@ test_rdma_dump_command_fn (vlib_main_t * vm, unformat_input_t * input, vnm, &sw_if_index)) ; else - return clib_error_return (0, "unknown input `%U'", - format_unformat_error, input); + { + clib_error_t *error = clib_error_return ( + 0, "unknown input `%U'", format_unformat_error, input); + unformat_free (line_input); + return error; + } } unformat_free (line_input); diff --git a/src/plugins/rdma/device.c b/src/plugins/rdma/device.c index 1cd9e2d2755b..4207f36e038d 100644 --- a/src/plugins/rdma/device.c +++ b/src/plugins/rdma/device.c @@ -363,10 +363,43 @@ rdma_unregister_interface (vnet_main_t * vnm, rdma_device_t * rd) ethernet_delete_interface (vnm, rd->hw_if_index); } +static void +rdma_rxq_free_buffers (vlib_main_t *vm, rdma_rxq_t *rxq) +{ + u32 owner_tail = clib_max (rxq->tail, rxq->incomplete_tail); + u32 n_owned = owner_tail - rxq->head; + + if (rxq->bufs && n_owned <= rxq->size) + vlib_buffer_free_from_ring (vm, rxq->bufs, rxq->head & (rxq->size - 1), + rxq->size, n_owned); + + if (rxq->second_bufs) + for (u32 i = 0; i < rxq->size; i++) + if (rxq->n_used_per_chain[i] < rxq->n_ds_per_wqe - 1) + vlib_buffer_free (vm, rxq->second_bufs + i, 1); + + vec_free (rxq->bufs); + vec_free (rxq->second_bufs); + vec_free (rxq->n_used_per_chain); +} + +static void +rdma_txq_free_buffers (vlib_main_t *vm, rdma_txq_t *txq) +{ + u32 n_owned = RDMA_TXQ_USED_SZ (txq->head, txq->tail); + u32 size = RDMA_TXQ_BUF_SZ (txq); + + if (txq->bufs && n_owned <= size) + vlib_buffer_free_from_ring (vm, txq->bufs, txq->head & (size - 1), + size, n_owned); + vec_free (txq->bufs); +} + static void rdma_dev_cleanup (rdma_device_t * rd) { rdma_main_t *rm = &rdma_main; + vlib_main_t *vm = vlib_get_main (); rdma_rxq_t *rxq; rdma_txq_t *txq; @@ -381,20 +414,24 @@ rdma_dev_cleanup (rdma_device_t * rd) _(ibv_destroy_flow, rd->flow_ucast6); _(ibv_destroy_flow, rd->flow_mcast4); _(ibv_destroy_flow, rd->flow_ucast4); - _(ibv_dereg_mr, rd->mr); + _(ibv_destroy_qp, rd->rx_qp6); + _(ibv_destroy_qp, rd->rx_qp4); + _(ibv_destroy_rwq_ind_table, rd->rx_rwq_ind_tbl); vec_foreach (txq, rd->txqs) - { _(ibv_destroy_qp, txq->qp); - _(ibv_destroy_cq, txq->cq); - } vec_foreach (rxq, rd->rxqs) - { _(ibv_destroy_wq, rxq->wq); + + vec_foreach (rxq, rd->rxqs) + rdma_rxq_free_buffers (vm, rxq); + vec_foreach (txq, rd->txqs) + rdma_txq_free_buffers (vm, txq); + + vec_foreach (txq, rd->txqs) + _(ibv_destroy_cq, txq->cq); + vec_foreach (rxq, rd->rxqs) _(ibv_destroy_cq, rxq->cq); - } - _(ibv_destroy_rwq_ind_table, rd->rx_rwq_ind_tbl); - _(ibv_destroy_qp, rd->rx_qp6); - _(ibv_destroy_qp, rd->rx_qp4); + _(ibv_dereg_mr, rd->mr); _(ibv_dealloc_pd, rd->pd); _(ibv_close_device, rd->ctx); #undef _ @@ -404,6 +441,7 @@ rdma_dev_cleanup (rdma_device_t * rd) vec_free (rd->rxqs); vec_free (rd->txqs); vec_free (rd->name); + vec_free (rd->linux_ifname); vlib_pci_free_device_info (rd->pci); pool_put (rm->devices, rd); } @@ -669,7 +707,11 @@ rdma_rxq_finalize (vlib_main_t *vm, rdma_device_t *rd) rwqia.log_ind_tbl_size = min_log2 (vec_len (ind_tbl)); rwqia.ind_tbl = ind_tbl; if ((rd->rx_rwq_ind_tbl = ibv_create_rwq_ind_table (rd->ctx, &rwqia)) == 0) - return clib_error_return_unix (0, "RWQ indirection table create failed"); + { + vec_free (ind_tbl); + return clib_error_return_unix (0, + "RWQ indirection table create failed"); + } vec_free (ind_tbl); memset (&qpia, 0, sizeof (qpia)); @@ -910,12 +952,17 @@ rdma_create_if (vlib_main_t * vm, rdma_create_if_args_t * args) } dev_list = ibv_get_device_list (&n_devs); + if (!dev_list) + { + args->error = clib_error_return_unix (0, "failed to get RDMA device list"); + goto err0; + } if (n_devs == 0) { args->error = clib_error_return_unix (0, "no RDMA devices available. Is the ib_uverbs module loaded?"); - goto err0; + goto err1; } /* get PCI address */ @@ -972,6 +1019,12 @@ rdma_create_if (vlib_main_t * vm, rdma_create_if_args_t * args) break; } + if (!rd->ctx) + { + args->error = clib_error_return_unix (0, "failed to open RDMA device"); + goto err2; + } + if (args->mode != RDMA_MODE_IBV) { struct mlx5dv_context mlx5dv_attrs = { }; @@ -1042,6 +1095,7 @@ are explicitly disabled, and if the interface supports it.*/ rd->rxqs[qid].queue_index = queue_index; } vnet_hw_if_update_runtime_data (vnm, rd->hw_if_index); + ibv_free_device_list (dev_list); vec_free (s); return; diff --git a/src/plugins/rdma/input.c b/src/plugins/rdma/input.c index d6add7442cbd..b0e142519cc9 100644 --- a/src/plugins/rdma/input.c +++ b/src/plugins/rdma/input.c @@ -530,15 +530,21 @@ rdma_device_poll_cq_mlx5dv (rdma_device_t * rd, rdma_rxq_t * rxq, /* partially processed mini-cqe array */ u32 n_mini_cqes = rxq->n_mini_cqes; u32 n_mini_cqes_left = rxq->n_mini_cqes_left; + u32 n_process = clib_min (n_mini_cqes_left, VLIB_FRAME_SIZE); + process_mini_cqes (rxq, n_mini_cqes - n_mini_cqes_left, - n_mini_cqes_left, cq_ci, mask, byte_cnt); + n_process, cq_ci, mask, byte_cnt); + clib_memset_u16 (cqe_flags, rxq->last_cqe_flags, n_process); + n_rx_packets = n_process; + byte_cnt += n_process; + cqe_flags += n_process; + rxq->n_mini_cqes_left -= n_process; + + if (rxq->n_mini_cqes_left) + return n_rx_packets; + compressed_cqe_reset_owner (rxq, n_mini_cqes, cq_ci, mask, log2_cq_size); - clib_memset_u16 (cqe_flags, rxq->last_cqe_flags, n_mini_cqes_left); - n_rx_packets = n_mini_cqes_left; - byte_cnt += n_mini_cqes_left; - cqe_flags += n_mini_cqes_left; - rxq->n_mini_cqes_left = 0; rxq->cq_ci = cq_ci = cq_ci + n_mini_cqes; } From 90a4f3e3b074c8a3765234ff041fe3ba85a18c80 Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 21 Jul 2026 06:38:04 +0800 Subject: [PATCH 27/85] rdma: validate device configuration Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/plugins/rdma/rdma.h | 2 +- src/plugins/rdma/unformat.c | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/plugins/rdma/rdma.h b/src/plugins/rdma/rdma.h index 216b4c696f54..e41f39859b72 100644 --- a/src/plugins/rdma/rdma.h +++ b/src/plugins/rdma/rdma.h @@ -95,7 +95,7 @@ typedef struct u32 incomplete_tail; /* Legacy RQ: tail index in bufs, corresponds to buffer chains with recycled valid head buffer, but whose other buffers are not yet recycled (due to pool exhaustion). */ - u16 n_total_additional_segs; + u32 n_total_additional_segs; u8 n_ds_per_wqe; /* Legacy RQ: number of nonnull data segs per WQE */ }; }; diff --git a/src/plugins/rdma/unformat.c b/src/plugins/rdma/unformat.c index 54397f6dfdb6..e06ca7598fd2 100644 --- a/src/plugins/rdma/unformat.c +++ b/src/plugins/rdma/unformat.c @@ -64,5 +64,10 @@ unformat_rdma_create_if_args (unformat_input_t * input, va_list * vargs) } unformat_free (line_input); + if (!ret) + { + vec_free (args->ifname); + vec_free (args->name); + } return ret; } From 79ec9cfb2af35a48b0c71ff4d1cdd308ea78cac8 Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 21 Jul 2026 06:38:04 +0800 Subject: [PATCH 28/85] time: keep cpu accounting monotonic Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/plugins/unittest/time_test.c | 47 +++++++++++++++++++++++++++++--- src/vppinfra/time.c | 24 +++++++--------- 2 files changed, 53 insertions(+), 18 deletions(-) diff --git a/src/plugins/unittest/time_test.c b/src/plugins/unittest/time_test.c index 2a1cba5f4e7b..57c2067b1782 100644 --- a/src/plugins/unittest/time_test.c +++ b/src/plugins/unittest/time_test.c @@ -47,6 +47,30 @@ test_time_monotonicity (vlib_main_t *vm) return 0; } +static clib_error_t * +test_time_rejected_frequency_sample (vlib_main_t *vm) +{ + clib_time_t ct; + u64 before, elapsed; + + clib_time_init (&ct); + elapsed = (u64) ct.clocks_per_second; + ct.total_cpu_time += 1000 * elapsed; + before = ct.total_cpu_time; + ct.last_cpu_time -= elapsed; + ct.last_verify_cpu_time -= 100 * elapsed; + + clib_time_verify_frequency (&ct); + + if (ct.total_cpu_time < before + elapsed / 2) + return clib_error_return ( + 0, "rejected sample lost elapsed CPU ticks: before %llu, now %llu", + before, ct.total_cpu_time); + + vlib_cli_output (vm, "Rejected frequency sample preserved elapsed ticks"); + return 0; +} + /* * Test that large CPU time discontinuities (e.g., from CPU migration) * are handled correctly and don't cause integer underflow. @@ -203,6 +227,7 @@ test_time_command_fn (vlib_main_t *vm, unformat_input_t *input, vlib_cli_command clib_error_t *error = 0; int test_monotonicity = 0; int test_discontinuity = 0; + int test_rejected_frequency = 0; int test_barrier = 0; int test_all = 0; @@ -212,6 +237,8 @@ test_time_command_fn (vlib_main_t *vm, unformat_input_t *input, vlib_cli_command test_monotonicity = 1; else if (unformat (input, "discontinuity")) test_discontinuity = 1; + else if (unformat (input, "rejected-frequency")) + test_rejected_frequency = 1; else if (unformat (input, "barrier")) test_barrier = 1; else if (unformat (input, "all")) @@ -221,10 +248,14 @@ test_time_command_fn (vlib_main_t *vm, unformat_input_t *input, vlib_cli_command } if (test_all) - test_monotonicity = test_discontinuity = test_barrier = 1; + test_monotonicity = test_discontinuity = test_rejected_frequency = + test_barrier = 1; - if (!test_monotonicity && !test_discontinuity && !test_barrier) - return clib_error_return (0, "specify test: monotonicity | discontinuity | barrier | all"); + if (!test_monotonicity && !test_discontinuity && !test_rejected_frequency && + !test_barrier) + return clib_error_return ( + 0, "specify test: monotonicity | discontinuity | rejected-frequency | " + "barrier | all"); if (test_monotonicity) { @@ -240,6 +271,13 @@ test_time_command_fn (vlib_main_t *vm, unformat_input_t *input, vlib_cli_command return error; } + if (test_rejected_frequency) + { + error = test_time_rejected_frequency_sample (vm); + if (error) + return error; + } + if (test_barrier) { error = test_barrier_time_monotonicity (vm); @@ -253,7 +291,8 @@ test_time_command_fn (vlib_main_t *vm, unformat_input_t *input, vlib_cli_command VLIB_CLI_COMMAND (test_time_command, static) = { .path = "test time", - .short_help = "test time [monotonicity | discontinuity | barrier | all]", + .short_help = "test time [monotonicity | discontinuity | " + "rejected-frequency | barrier | all]", .function = test_time_command_fn, .is_mp_safe = 1, }; diff --git a/src/vppinfra/time.c b/src/vppinfra/time.c index 36e9a4fa11fb..8a88f7f369d3 100644 --- a/src/vppinfra/time.c +++ b/src/vppinfra/time.c @@ -218,6 +218,7 @@ clib_time_verify_frequency (clib_time_t * c) { f64 now_reference, delta_reference, delta_reference_max; f64 delta_clock_in_seconds; + f64 elapsed_time; u64 now_clock, delta_clock; f64 new_clocks_per_second, delta; @@ -230,7 +231,8 @@ clib_time_verify_frequency (clib_time_t * c) * lower TSC). In this case, skip frequency estimation and just resync * timestamps to current values. */ - if (PREDICT_FALSE (now_clock < c->last_verify_cpu_time)) + if (PREDICT_FALSE (now_clock < c->last_verify_cpu_time || + now_clock < c->last_cpu_time)) { c->last_cpu_time = now_clock; c->last_verify_cpu_time = now_clock; @@ -246,15 +248,11 @@ clib_time_verify_frequency (clib_time_t * c) delta_clock_in_seconds = (f64) (now_clock - c->last_verify_cpu_time) * c->seconds_per_clock; - /* - * Recompute vpp start time reference, and total clocks - * using the current clock rate. - * Ensure total_cpu_time never decreases to guarantee monotonicity. - */ + /* Recompute vpp start time reference using the current clock rate. */ c->init_reference_time += (delta_reference - delta_clock_in_seconds); - c->total_cpu_time = clib_max ( - c->total_cpu_time, (u64) ((now_reference - c->init_reference_time) * c->clocks_per_second)); + /* Account for elapsed clocks before advancing the sample baseline. */ + c->total_cpu_time += now_clock - c->last_cpu_time; c->last_cpu_time = now_clock; /* Calculate a new clock rate sample */ @@ -295,18 +293,16 @@ clib_time_verify_frequency (clib_time_t * c) return; } + elapsed_time = c->total_cpu_time * c->seconds_per_clock; + /* Add sample to the exponentially-smoothed rate */ c->clocks_per_second = c->clocks_per_second * c->damping_constant + (1.0 - c->damping_constant) * new_clocks_per_second; c->seconds_per_clock = 1.0 / c->clocks_per_second; - /* - * Recalculate total_cpu_time based on the kernel timebase, and - * the calculated clock rate. - * Ensure total_cpu_time never decreases to guarantee monotonicity. - */ + /* Keep reported elapsed time continuous across the frequency change. */ c->total_cpu_time = clib_max ( - c->total_cpu_time, (u64) ((now_reference - c->init_reference_time) * c->clocks_per_second)); + c->total_cpu_time, (u64) (elapsed_time * c->clocks_per_second)); } From 93e1dbf02adc733b234286d7ea06fd4d613f69e4 Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 21 Jul 2026 06:38:04 +0800 Subject: [PATCH 29/85] svm: report fifo attach failures Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/svm/fifo_segment.c | 29 ++++++++++++++++++++++++----- src/svm/fifo_segment.h | 4 ++-- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/svm/fifo_segment.c b/src/svm/fifo_segment.c index f04a67ce6caf..5f569f5548cc 100644 --- a/src/svm/fifo_segment.c +++ b/src/svm/fifo_segment.c @@ -1038,10 +1038,11 @@ fifo_segment_detach_fifo (fifo_segment_t *fs, svm_fifo_t **f) *f = 0; } -void +int fifo_segment_attach_fifo (fifo_segment_t *fs, svm_fifo_t **f, u32 slice_index) { - svm_fifo_chunk_t *c, *nc, *pc = 0; + svm_fifo_chunk_t *c, *nc, *pc = 0, *attach_chunks = 0; + svm_fifo_shared_t *attach_hdr = 0; fifo_slice_private_t *pfss; fifo_segment_slice_t *fss; svm_fifo_t *nf, *of; @@ -1059,21 +1060,39 @@ fifo_segment_attach_fifo (fifo_segment_t *fs, svm_fifo_t **f, u32 slice_index) /* Allocate shared hdr and chunks to be collected at detach in return * for those that are being attached now */ of = *f; - svm_fifo_hdr_at_attach (of) = fsh_try_alloc_fifo_hdr (fs->h, fss); + attach_hdr = fsh_try_alloc_fifo_hdr (fs->h, fss); + if (!attach_hdr) + goto error; c = fs_chunk_ptr (fs->h, nf->shr->start_chunk); - svm_fifo_chunks_at_attach (of) = pc = - fsh_try_alloc_chunk (fs->h, fss, c->length); + attach_chunks = pc = fsh_try_alloc_chunk (fs->h, fss, c->length); + if (!attach_chunks) + goto error; while ((c = fs_chunk_ptr (fs->h, c->next))) { nc = fsh_try_alloc_chunk (fs->h, fss, c->length); + if (!nc) + goto error; pc->next = fs_chunk_sptr (fs->h, nc); pc = nc; } + svm_fifo_hdr_at_attach (of) = attach_hdr; + svm_fifo_chunks_at_attach (of) = attach_chunks; nf->shr->slice_index = slice_index; *f = nf; + return 0; + +error: + fsh_slice_collect_chunks (fs->h, fss, attach_chunks); + if (attach_hdr) + fss_fifo_free_list_push (fs->h, fss, attach_hdr); + if (nf->flags & SVM_FIFO_F_LL_TRACKED) + pfss_fifo_del_active_list (pfss, nf); + fss->virtual_mem -= svm_fifo_size (nf); + fs_fifo_free (fs, nf, slice_index); + return -1; } uword diff --git a/src/svm/fifo_segment.h b/src/svm/fifo_segment.h index 90ea7be49754..52a63c773999 100644 --- a/src/svm/fifo_segment.h +++ b/src/svm/fifo_segment.h @@ -140,8 +140,8 @@ void fifo_segment_free_fifo (fifo_segment_t * fs, svm_fifo_t * f); void fifo_segment_free_client_fifo (fifo_segment_t *fs, svm_fifo_t *f); void fifo_segment_detach_fifo (fifo_segment_t *fs, svm_fifo_t **f); -void fifo_segment_attach_fifo (fifo_segment_t *fs, svm_fifo_t **f, - u32 slice_index); +int fifo_segment_attach_fifo (fifo_segment_t *fs, svm_fifo_t **f, + u32 slice_index); uword fifo_segment_fifo_offset (svm_fifo_t *f); /** From f9adf3ec3cdb8e64c4d098bc81bb79568b9ecd13 Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 21 Jul 2026 06:38:04 +0800 Subject: [PATCH 30/85] vlib: harden buffer and handoff cleanup Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/vlib/buffer_funcs.h | 13 ++++++++++--- src/vlib/handoff.c | 7 +++++-- src/vlib/node_funcs.h | 2 +- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/vlib/buffer_funcs.h b/src/vlib/buffer_funcs.h index 649e67a8f561..2d464a3dc98b 100644 --- a/src/vlib/buffer_funcs.h +++ b/src/vlib/buffer_funcs.h @@ -1419,11 +1419,18 @@ vlib_buffer_clone_at_offset (vlib_main_t * vm, u32 src_buffer, u32 * buffers, while (n_buffers > VLIB_BUFFER_MAX_CLONE) { vlib_buffer_t *copy; + u16 n_cloned_batch; + copy = vlib_buffer_copy (vm, s); if (copy) - n_cloned += - vlib_buffer_clone_255 (vm, vlib_get_buffer_index (vm, copy), (buffers + n_cloned), - VLIB_BUFFER_MAX_CLONE, head_end_offset, offset); + { + n_cloned_batch = vlib_buffer_clone_255 ( + vm, vlib_get_buffer_index (vm, copy), buffers + n_cloned, + VLIB_BUFFER_MAX_CLONE, head_end_offset, offset); + if (n_cloned_batch == 0) + vlib_buffer_free_one (vm, vlib_get_buffer_index (vm, copy)); + n_cloned += n_cloned_batch; + } n_buffers -= VLIB_BUFFER_MAX_CLONE; } n_cloned += vlib_buffer_clone_255 (vm, src_buffer, buffers + n_cloned, diff --git a/src/vlib/handoff.c b/src/vlib/handoff.c index 779a3df6a456..e061661c2ecb 100644 --- a/src/vlib/handoff.c +++ b/src/vlib/handoff.c @@ -204,7 +204,7 @@ vlib_handoff_queue_dequeue_inline (vlib_main_t *vm, vlib_handoff_queue_main_t *h if (n_deq) { hq->n_vectors += n_deq; - __atomic_store_n (&hq->head, head, __ATOMIC_RELAXED); + __atomic_store_n (&hq->head, head, __ATOMIC_RELEASE); if (n_deq >= dequeue_vector_limit) __atomic_fetch_or (&vm->handoff_queue_pending_bmp, hqm->queue_bit, __ATOMIC_RELAXED); } @@ -355,7 +355,7 @@ vlib_handoff_enqueue_one_thread (vlib_main_t *vm, vlib_node_runtime_t *node, retry: tail = __atomic_load_n (&hq->tail, __ATOMIC_RELAXED); - head = __atomic_load_n (&hq->head, __ATOMIC_RELAXED); + head = __atomic_load_n (&hq->head, __ATOMIC_ACQUIRE); n_avail = head + size - tail; if (n_avail < n_req) @@ -739,6 +739,7 @@ vlib_handoff_alloc_queues (vlib_handoff_alloc_queues_args_t *a) } hqm_index = vec_len (vm->handoff_queue_mains); + vlib_worker_thread_barrier_sync (this_vm); vec_add2 (vm->handoff_queue_mains, hqm_at_index, 1); hqm_at_index[0] = hqm; @@ -753,6 +754,8 @@ vlib_handoff_alloc_queues (vlib_handoff_alloc_queues_args_t *a) vm->handoff_queue_mains = this_vm->handoff_queue_mains; } + vlib_worker_thread_barrier_release (this_vm); + return hqm_index; } #endif diff --git a/src/vlib/node_funcs.h b/src/vlib/node_funcs.h index 7d975c6a6cb0..5c53a1edf7a5 100644 --- a/src/vlib/node_funcs.h +++ b/src/vlib/node_funcs.h @@ -1128,7 +1128,7 @@ vlib_process_signal_event_at_time (vlib_main_t * vm, ASSERT (te->n_data_elt_bytes == n_data_elt_bytes); ASSERT (te->n_data_bytes == n_data_elts * n_data_elt_bytes); - te->process_node_index = n->runtime_index; + te->process_node_index = n->index; te->event_type_index = t; p->stop_timer_handle = From 02252fa24fecc437dd3b9096aca9feed206adb68 Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 21 Jul 2026 06:38:24 +0800 Subject: [PATCH 31/85] mem: roll back failed mappings safely Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/vlib/physmem.c | 8 ++++---- src/vppinfra/linux/mem.c | 22 +++++++++++++--------- src/vppinfra/mem_dlmalloc.c | 13 +++++++++++++ 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/src/vlib/physmem.c b/src/vlib/physmem.c index afa753aeb14c..a842230e26e0 100644 --- a/src/vlib/physmem.c +++ b/src/vlib/physmem.c @@ -57,14 +57,14 @@ vlib_physmem_shared_map_create (vlib_main_t * vm, char *name, uword size, map->log2_page_size = a->log2_subpage_sz; map->numa_node = a->numa_node; - for (i = 0; i < a->n_pages; i++) + for (i = 0; i < map->n_pages; i++) { - uword pa = - clib_pmalloc_get_pa (pm, (u8 *) va + (i << a->log2_subpage_sz)); + void *page_va = (u8 *) va + (i << a->log2_subpage_sz); + uword pa = clib_pmalloc_get_pa (pm, page_va); /* maybe iova */ if (pa == 0) - pa = pointer_to_uword (va); + pa = pointer_to_uword (page_va); vec_add1 (map->page_table, pa); } diff --git a/src/vppinfra/linux/mem.c b/src/vppinfra/linux/mem.c index ce2334288193..7a3dd058c380 100644 --- a/src/vppinfra/linux/mem.c +++ b/src/vppinfra/linux/mem.c @@ -311,6 +311,7 @@ clib_mem_vm_map_internal (void *base, clib_mem_page_sz_t log2_page_sz, { clib_mem_main_t *mm = &clib_mem_main; clib_mem_vm_map_hdr_t *hdr; + void *reserved_base; uword sys_page_sz = 1ULL << mm->log2_page_sz; int mmap_flags = MAP_FIXED, is_huge = 0; @@ -353,21 +354,21 @@ clib_mem_vm_map_internal (void *base, clib_mem_page_sz_t log2_page_sz, size = round_pow2 (size, 1ULL << log2_page_sz); - base = (void *) clib_mem_vm_reserve ((uword) base, size, - clib_max (log2_page_sz, log2_align)); + reserved_base = (void *) clib_mem_vm_reserve ( + (uword) base, size, clib_max (log2_page_sz, log2_align)); - if (base == (void *) ~0) + if (reserved_base == (void *) ~0) return CLIB_MEM_VM_MAP_FAILED; - base = mmap (base, size, PROT_READ | PROT_WRITE, mmap_flags, fd, offset); + base = mmap (reserved_base, size, PROT_READ | PROT_WRITE, mmap_flags, fd, + offset); if (base == MAP_FAILED) - return CLIB_MEM_VM_MAP_FAILED; + goto error; if (is_huge && (mlock (base, size) != 0)) { - munmap (base, size); - return CLIB_MEM_VM_MAP_FAILED; + goto error; } hdr = mmap (base - sys_page_sz, sys_page_sz, PROT_READ | PROT_WRITE, @@ -375,8 +376,7 @@ clib_mem_vm_map_internal (void *base, clib_mem_page_sz_t log2_page_sz, if (hdr != base - sys_page_sz) { - munmap (base, size); - return CLIB_MEM_VM_MAP_FAILED; + goto error; } map_lock (); @@ -408,6 +408,10 @@ clib_mem_vm_map_internal (void *base, clib_mem_page_sz_t log2_page_sz, clib_mem_unpoison (base, size); return base; + +error: + munmap (reserved_base - sys_page_sz, size + sys_page_sz); + return CLIB_MEM_VM_MAP_FAILED; } __clib_export int diff --git a/src/vppinfra/mem_dlmalloc.c b/src/vppinfra/mem_dlmalloc.c index 40a707b595f2..60e06ce872df 100644 --- a/src/vppinfra/mem_dlmalloc.c +++ b/src/vppinfra/mem_dlmalloc.c @@ -79,11 +79,24 @@ clib_mem_create_heap_internal (void *base, uword size, h.size = size; h.log2_page_sz = log2_page_sz; h.mspace = create_mspace_with_base (base, size, is_locked); + if (h.mspace == 0) + { + if (h.unmap_on_destroy) + clib_mem_vm_unmap (base); + return 0; + } mspace_disable_expand (h.mspace); clib_mem_poison (mspace_least_addr (h.mspace), mspace_footprint (h.mspace)); sz = round_pow2 (sizeof (clib_mem_heap_t) + strlen (name) + 1, align); hp = mspace_memalign (h.mspace, align, sz); + if (hp == 0) + { + destroy_mspace (h.mspace); + if (h.unmap_on_destroy) + clib_mem_vm_unmap (base); + return 0; + } clib_mem_unpoison (hp, sz); *hp = h; From ec58fd760de064e4edd36f1766af434198540278 Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 21 Jul 2026 06:38:24 +0800 Subject: [PATCH 32/85] api: bound traced message copies Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/vlibapi/api_shared.c | 51 +++++++++++++++++++++++++++++++--------- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/src/vlibapi/api_shared.c b/src/vlibapi/api_shared.c index 9cb9896993e2..3a83fe290d97 100644 --- a/src/vlibapi/api_shared.c +++ b/src/vlibapi/api_shared.c @@ -58,20 +58,30 @@ vl_msg_api_tx_trace_enabled (api_main_t * am) /* * vl_msg_api_trace */ -void -vl_msg_api_trace (api_main_t * am, vl_api_trace_t * tp, void *msg) +static void +vl_msg_api_trace_with_size (api_main_t *am, vl_api_trace_t *tp, void *msg, + uword msg_len) { u8 **this_trace; u8 **old_trace; u8 *msg_copy; - u32 length; - u16 msg_id = clib_net_to_host_u16 (*((u16 *) msg)); - vl_api_msg_data_t *m = vl_api_get_msg_data (am, msg_id); - msgbuf_t *header = (msgbuf_t *) (((u8 *) msg) - offsetof (msgbuf_t, data)); + uword length; + u16 msg_id; + vl_api_msg_data_t *m; + + if (PREDICT_FALSE (msg_len < sizeof (u16))) + return; + msg_id = clib_net_to_host_u16 (*((u16 *) msg)); + m = vl_api_get_msg_data (am, msg_id); if (!m || !m->trace_enable) return; + length = msg_len; + if (m->trace_size >= (int) sizeof (u16) && + length > (uword) m->trace_size) + length = m->trace_size; + msg_copy = 0; if (tp->nitems == 0) @@ -97,13 +107,20 @@ vl_msg_api_trace (api_main_t * am, vl_api_trace_t * tp, void *msg) this_trace = old_trace; } - length = clib_net_to_host_u32 (header->data_len); - vec_validate (msg_copy, length - 1); clib_memcpy_fast (msg_copy, msg, length); *this_trace = msg_copy; } +void +vl_msg_api_trace (api_main_t *am, vl_api_trace_t *tp, void *msg) +{ + msgbuf_t *header = (msgbuf_t *) (((u8 *) msg) - offsetof (msgbuf_t, data)); + u32 msg_len = clib_net_to_host_u32 (header->data_len); + + vl_msg_api_trace_with_size (am, tp, msg, msg_len); +} + int vl_msg_api_trace_onoff (api_main_t * am, vl_api_trace_which_t which, int onoff) @@ -211,6 +228,8 @@ vl_msg_api_trace_write_one (api_main_t *am, u8 *msg, FILE *fp) int tlen, slen; u32 msg_length = vec_len (msg); + if (PREDICT_FALSE (msg_length < sizeof (u16))) + return -1; vec_validate (tmpmem, msg_length - 1); clib_memcpy_fast (tmpmem, msg, msg_length); u16 id = clib_net_to_host_u16 (*((u16 *) msg)); @@ -484,8 +503,18 @@ always_inline void msg_handler_internal (api_main_t *am, void *the_msg, uword msg_len, int trace_it, int do_it, int free_it) { - u16 id = clib_net_to_host_u16 (*((u16 *) the_msg)); - vl_api_msg_data_t *m = vl_api_get_msg_data (am, id); + u16 id; + vl_api_msg_data_t *m; + + if (PREDICT_FALSE (msg_len < sizeof (u16))) + { + if (free_it) + vl_msg_api_free (the_msg); + return; + } + + id = clib_net_to_host_u16 (*((u16 *) the_msg)); + m = vl_api_get_msg_data (am, id); if (PREDICT_FALSE (am->elog_trace_api_messages)) { @@ -508,7 +537,7 @@ msg_handler_internal (api_main_t *am, void *the_msg, uword msg_len, if (m && m->handler) { if (trace_it) - vl_msg_api_trace (am, am->rx_trace, the_msg); + vl_msg_api_trace_with_size (am, am->rx_trace, the_msg, msg_len); if (am->msg_print_flag) { From b941447b5728b4c496295d0fda0e110ae811b8d0 Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 21 Jul 2026 06:38:24 +0800 Subject: [PATCH 33/85] dpo: validate replicate counters Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/vnet/dpo/replicate_dpo.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vnet/dpo/replicate_dpo.c b/src/vnet/dpo/replicate_dpo.c index 834fe1df2304..3267c26c701d 100644 --- a/src/vnet/dpo/replicate_dpo.c +++ b/src/vnet/dpo/replicate_dpo.c @@ -751,7 +751,7 @@ replicate_inline (vlib_main_t * vm, const replicate_t *rep0; vlib_buffer_t * b0, *c0; const dpo_id_t *dpo0; - u8 num_cloned; + u16 num_cloned; bi0 = from[0]; from += 1; From 6e691e8855b9b677e26ad4dad2050c74b87a398e Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 21 Jul 2026 06:38:24 +0800 Subject: [PATCH 34/85] session: propagate fifo migration failures Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/vnet/session/segment_manager.c | 13 +++++++++---- src/vnet/session/segment_manager.h | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/vnet/session/segment_manager.c b/src/vnet/session/segment_manager.c index 8ddd38d7ca33..ef6c5d33e6fe 100644 --- a/src/vnet/session/segment_manager.c +++ b/src/vnet/session/segment_manager.c @@ -965,21 +965,24 @@ segment_manager_detach_fifo (svm_fifo_t **f) segment_manager_segment_reader_unlock (sm); } -void +int segment_manager_attach_fifo (svm_fifo_t **f, session_t *s) { segment_manager_t *sm; fifo_segment_t *fs; + int rv; sm = segment_manager_get_if_valid ((*f)->segment_manager); if (PREDICT_FALSE (!sm)) - return; + return -1; fs = segment_manager_get_segment_w_lock (sm, (*f)->segment_index); - fifo_segment_attach_fifo (fs, f, s->thread_index); + rv = fifo_segment_attach_fifo (fs, f, s->thread_index); segment_manager_segment_reader_unlock (sm); - (*f)->vpp_sh = s->handle; + if (rv == 0) + (*f)->vpp_sh = s->handle; + return rv; } u32 @@ -1465,6 +1468,7 @@ sm_lookup_free_custom_segment (segment_manager_t *sm, u32 seg_ctx_index) seg_ctx = pool_elt_at_index (smm->custom_seg_ctxs, seg_ctx_index); max_free_bytes = seg_ctx->fifo_pair_bytes; + segment_manager_segment_reader_lock (sm); pool_foreach (ct_seg, seg_ctx->segments) { /* Client or server has detached so segment cannot be used */ @@ -1480,6 +1484,7 @@ sm_lookup_free_custom_segment (segment_manager_t *sm, u32 seg_ctx_index) res = ct_seg; } } + segment_manager_segment_reader_unlock (sm); return res; } diff --git a/src/vnet/session/segment_manager.h b/src/vnet/session/segment_manager.h index f53459a17dbc..7f962327ed05 100644 --- a/src/vnet/session/segment_manager.h +++ b/src/vnet/session/segment_manager.h @@ -145,7 +145,7 @@ int segment_manager_alloc_session_fifos_ct (session_t *s, void segment_manager_dealloc_fifos (svm_fifo_t *rx_fifo, svm_fifo_t *tx_fifo); void segment_manager_dealloc_fifos_ct (svm_fifo_t *rx_fifo, svm_fifo_t *tx_fifo, u32 is_client); void segment_manager_detach_fifo (svm_fifo_t **f); -void segment_manager_attach_fifo (svm_fifo_t **f, session_t *s); +int segment_manager_attach_fifo (svm_fifo_t **f, session_t *s); void segment_manager_set_watermarks (segment_manager_t * sm, u8 high_watermark, u8 low_watermark); From 98027d19c0e1e162e03440e23222789a1982c2a7 Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 21 Jul 2026 06:38:24 +0800 Subject: [PATCH 35/85] session: roll back failed fifo migration Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/vnet/session/session.c | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/vnet/session/session.c b/src/vnet/session/session.c index e447f60bf07c..19e927006171 100644 --- a/src/vnet/session/session.c +++ b/src/vnet/session/session.c @@ -921,6 +921,7 @@ session_dgram_connect_notify (transport_connection_t *tc, session_handle_tu_t osh, session_t **new_session) { session_t *new_s; + int rx_attached = 0; /* * Clone half-open session to the right thread. @@ -937,8 +938,11 @@ session_dgram_connect_notify (transport_connection_t *tc, if (!(new_s->flags & SESSION_F_PROXY)) { /* New set of fifos attached to the same shared memory */ - segment_manager_attach_fifo (&new_s->rx_fifo, new_s); - segment_manager_attach_fifo (&new_s->tx_fifo, new_s); + if (segment_manager_attach_fifo (&new_s->rx_fifo, new_s) < 0) + goto attach_error; + rx_attached = 1; + if (segment_manager_attach_fifo (&new_s->tx_fifo, new_s) < 0) + goto attach_error; } /* @@ -952,6 +956,14 @@ session_dgram_connect_notify (transport_connection_t *tc, new_s->connection_index = tc->c_index; *new_session = new_s; return 0; + +attach_error: + if (rx_attached) + segment_manager_detach_fifo (&new_s->rx_fifo); + if (!(tc->flags & TRANSPORT_CONNECTION_F_NO_LOOKUP)) + session_lookup_del_connection (tc); + session_free (new_s); + return -1; } /** From ddda3eb058449757f1ea045857e4622989b23f2e Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 21 Jul 2026 06:38:24 +0800 Subject: [PATCH 36/85] timer: bound hierarchical expirations Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/vppinfra/test_tw_timer.c | 70 ++++++++++++++++- src/vppinfra/tw_timer_template.c | 131 ++++++++++++++++++++----------- 2 files changed, 153 insertions(+), 48 deletions(-) diff --git a/src/vppinfra/test_tw_timer.c b/src/vppinfra/test_tw_timer.c index 72a7f45d39f6..13ecf4f589ba 100644 --- a/src/vppinfra/test_tw_timer.c +++ b/src/vppinfra/test_tw_timer.c @@ -54,6 +54,14 @@ typedef struct tw_timer_test_main_t tw_timer_test_main; +static u32 safety_expired_count; + +static void +expired_timer_safety_callback (u32 * expired_timers) +{ + safety_expired_count += vec_len (expired_timers); +} + static void run_single_wheel (tw_timer_wheel_2t_1w_2048sl_t * tw, u32 n_ticks) { @@ -1318,6 +1326,60 @@ test5_double (tw_timer_test_main_t * tm) return 0; } +static clib_error_t * +test6_safety (tw_timer_test_main_t * tm) +{ + tw_timer_wheel_2t_1w_2048sl_t tw; + f64 first_run, large_gap; + + (void) tm; + + tw_timer_wheel_init_2t_1w_2048sl (&tw, expired_timer_safety_callback, 1.0, + 1 /* max expirations */); + + /* Prime the wheel, then make a gap much larger than the wheel geometry. */ + tw_timer_expire_timers_2t_1w_2048sl (&tw, 1.0); + first_run = tw.last_run_time; + large_gap = first_run + 1e10; + tw_timer_expire_timers_2t_1w_2048sl (&tw, large_gap); + + if (tw.last_run_time != large_gap) + { + tw_timer_wheel_free_2t_1w_2048sl (&tw); + return clib_error_return (0, "empty wheel did not re-anchor after large gap"); + } + + safety_expired_count = 0; + tw_timer_start_2t_1w_2048sl (&tw, 0, 0, 1); + tw_timer_start_2t_1w_2048sl (&tw, 1, 0, 1); + tw_timer_expire_timers_2t_1w_2048sl (&tw, large_gap + 2.0); + + if (safety_expired_count != 1) + { + tw_timer_wheel_free_2t_1w_2048sl (&tw); + return clib_error_return (0, "max_expirations was not enforced: %u", + safety_expired_count); + } + + if (tw.last_run_time >= large_gap + 2.0) + { + tw_timer_wheel_free_2t_1w_2048sl (&tw); + return clib_error_return (0, "active timer backlog was discarded"); + } + + tw_timer_expire_timers_2t_1w_2048sl (&tw, large_gap + 2.0); + + if (safety_expired_count != 2) + { + tw_timer_wheel_free_2t_1w_2048sl (&tw); + return clib_error_return (0, "same-slot backlog was not preserved: %u", + safety_expired_count); + } + + tw_timer_wheel_free_2t_1w_2048sl (&tw); + return 0; +} + static clib_error_t * timer_test_command_fn (tw_timer_test_main_t * tm, unformat_input_t * input) { @@ -1328,6 +1390,7 @@ timer_test_command_fn (tw_timer_test_main_t * tm, unformat_input_t * input) int is_test3 = 0; int is_test4 = 0; int is_test5 = 0; + int is_test6 = 0; int overflow = 0; clib_memset (tm, 0, sizeof (*tm)); @@ -1353,6 +1416,8 @@ timer_test_command_fn (tw_timer_test_main_t * tm, unformat_input_t * input) is_test4 = 1; else if (unformat (input, "linear")) is_test5 = 1; + else if (unformat (input, "safety")) + is_test6 = 1; else if (unformat (input, "updates")) is_updates = 1; else if (unformat (input, "wheels %d", &num_wheels)) @@ -1367,7 +1432,7 @@ timer_test_command_fn (tw_timer_test_main_t * tm, unformat_input_t * input) break; } - if (is_test1 + is_test2 + is_test3 + is_test4 + is_test5 == 0) + if (is_test1 + is_test2 + is_test3 + is_test4 + is_test5 + is_test6 == 0) return clib_error_return (0, "No test specified [test1..n]"); if (num_wheels < 1 || num_wheels > 3) @@ -1409,6 +1474,9 @@ timer_test_command_fn (tw_timer_test_main_t * tm, unformat_input_t * input) if (is_test5) return test5_double (tm); + if (is_test6) + return test6_safety (tm); + /* NOTREACHED */ return 0; } diff --git a/src/vppinfra/tw_timer_template.c b/src/vppinfra/tw_timer_template.c index 6bedc4de92e6..8e11e51c4209 100644 --- a/src/vppinfra/tw_timer_template.c +++ b/src/vppinfra/tw_timer_template.c @@ -495,7 +495,8 @@ static inline f64 now, u32 * callback_vector_arg) { - u32 nticks, i; + u32 nticks, i, ticks_processed = 0, expirations_processed = 0; + f64 elapsed_ticks; tw_timer_wheel_slot_t *ts; TWT (tw_timer) * t, *head; u32 *callback_vector; @@ -508,22 +509,39 @@ static inline if (PREDICT_FALSE (now < tw->next_run_time)) return callback_vector_arg; - /* Number of ticks which have occurred */ - nticks = tw->ticks_per_second * (now - tw->last_run_time); - if (nticks == 0) - return callback_vector_arg; - - /* Remember when we ran, compute next runtime */ - tw->next_run_time = (now + tw->timer_interval); - /* First call, or time jumped backwards? */ if (PREDICT_FALSE ((tw->last_run_time == 0.0) || (now <= tw->last_run_time))) { tw->last_run_time = now; + tw->next_run_time = now + tw->timer_interval; return callback_vector_arg; } + /* An empty wheel has no expiration backlog to preserve. */ + if (pool_elts (tw->timers) == TW_TIMER_WHEELS * TW_SLOTS_PER_RING +#if TW_OVERFLOW_VECTOR > 0 + + 1 +#endif + ) + { + tw->last_run_time = now; + tw->next_run_time = now + tw->timer_interval; + return callback_vector_arg; + } + + /* Number of ticks which have occurred. Validate before converting to u32. */ + elapsed_ticks = tw->ticks_per_second * (now - tw->last_run_time); + if (!(elapsed_ticks > 0.0)) + return callback_vector_arg; + + if (elapsed_ticks >= (f64) (2 * TW_SLOTS_PER_RING)) + nticks = 2 * TW_SLOTS_PER_RING; + else + nticks = (u32) elapsed_ticks; + if (nticks == 0) + return callback_vector_arg; + if (callback_vector_arg == 0) { vec_set_len (tw->expired_timer_handles, 0); @@ -554,17 +572,13 @@ static inline head = pool_elt_at_index (tw->timers, ts->head_index); next_index = head->next; - /* Make slot empty */ - head->next = head->prev = ts->head_index; - /* traverse slot, place timers wherever they go */ while (next_index != head - tw->timers) { t = pool_elt_at_index (tw->timers, next_index); next_index = t->next; - /* Remove from the overflow vector (hammer) */ - t->next = t->prev = ~0; + timer_remove (tw->timers, t); ASSERT (t->expiration_time >= tw->current_tick); @@ -589,12 +603,19 @@ static inline t->slow_ring_offset = new_slow_ring_offset; t->fast_ring_offset = new_fast_ring_offset; - /* Timer expires Right Now */ - if (PREDICT_FALSE (t->slow_ring_offset == 0 && - t->fast_ring_offset == 0 && - new_glacier_ring_offset == 0)) - { - vec_add1 (callback_vector, t->user_handle); + /* Timer expires Right Now */ + if (PREDICT_FALSE (t->slow_ring_offset == 0 && + t->fast_ring_offset == 0 && + new_glacier_ring_offset == 0)) + { + if (expirations_processed + vec_len (callback_vector) >= + tw->max_expirations) + { + ts = &tw->w[TW_TIMER_RING_FAST][0]; + timer_addhead (tw->timers, ts->head_index, t - tw->timers); + break; + } + vec_add1 (callback_vector, t->user_handle); #if TW_START_STOP_TRACE_SIZE > 0 TW (tw_timer_trace) (tw, 0xfe, t->user_handle, t - tw->timers); @@ -642,23 +663,26 @@ static inline head = pool_elt_at_index (tw->timers, ts->head_index); next_index = head->next; - /* Make slot empty */ - head->next = head->prev = ts->head_index; - /* traverse slot, deal timers into slow ring */ while (next_index != head - tw->timers) { t = pool_elt_at_index (tw->timers, next_index); next_index = t->next; - /* Remove from glacier ring slot (hammer) */ - t->next = t->prev = ~0; - - /* Timer expires Right Now */ - if (PREDICT_FALSE (t->slow_ring_offset == 0 && - t->fast_ring_offset == 0)) - { - vec_add1 (callback_vector, t->user_handle); + timer_remove (tw->timers, t); + + /* Timer expires Right Now */ + if (PREDICT_FALSE (t->slow_ring_offset == 0 && + t->fast_ring_offset == 0)) + { + if (expirations_processed + vec_len (callback_vector) >= + tw->max_expirations) + { + ts = &tw->w[TW_TIMER_RING_FAST][0]; + timer_addhead (tw->timers, ts->head_index, t - tw->timers); + break; + } + vec_add1 (callback_vector, t->user_handle); #if TW_START_STOP_TRACE_SIZE > 0 TW (tw_timer_trace) (tw, 0xfe, t->user_handle, t - tw->timers); @@ -698,22 +722,25 @@ static inline head = pool_elt_at_index (tw->timers, ts->head_index); next_index = head->next; - /* Make slot empty */ - head->next = head->prev = ts->head_index; - /* traverse slot, deal timers into fast ring */ while (next_index != head - tw->timers) { t = pool_elt_at_index (tw->timers, next_index); next_index = t->next; - /* Remove from sloe ring slot (hammer) */ - t->next = t->prev = ~0; - - /* Timer expires Right Now */ - if (PREDICT_FALSE (t->fast_ring_offset == 0)) - { - vec_add1 (callback_vector, t->user_handle); + timer_remove (tw->timers, t); + + /* Timer expires Right Now */ + if (PREDICT_FALSE (t->fast_ring_offset == 0)) + { + if (expirations_processed + vec_len (callback_vector) >= + tw->max_expirations) + { + ts = &tw->w[TW_TIMER_RING_FAST][0]; + timer_addhead (tw->timers, ts->head_index, t - tw->timers); + break; + } + vec_add1 (callback_vector, t->user_handle); #if TW_START_STOP_TRACE_SIZE > 0 TW (tw_timer_trace) (tw, 0xfe, t->user_handle, t - tw->timers); @@ -742,14 +769,14 @@ static inline head = pool_elt_at_index (tw->timers, ts->head_index); next_index = head->next; - /* Make slot empty */ - head->next = head->prev = ts->head_index; - /* Construct vector of expired timer handles to give the user */ - while (next_index != ts->head_index) + while (next_index != ts->head_index && + expirations_processed + vec_len (callback_vector) < + tw->max_expirations) { t = pool_elt_at_index (tw->timers, next_index); next_index = t->next; + timer_remove (tw->timers, t); vec_add1 (callback_vector, t->user_handle); #if TW_START_STOP_TRACE_SIZE > 0 TW (tw_timer_trace) (tw, 0xfe, t->user_handle, t - tw->timers); @@ -763,12 +790,17 @@ static inline /* The callback is optional. We return the u32 * handle vector */ if (tw->expired_timer_callback) { + expirations_processed += vec_len (callback_vector); tw->expired_timer_callback (callback_vector); vec_reset_length (callback_vector); } tw->expired_timer_handles = callback_vector; } + /* Do not advance past a partially drained slot. */ + if (head->next != ts->head_index) + break; + #if TW_FAST_WHEEL_BITMAP tw->fast_slot_bitmap = clib_bitmap_set (tw->fast_slot_bitmap, fast_wheel_index, 0); @@ -790,14 +822,19 @@ static inline tw->current_index[TW_TIMER_RING_GLACIER] = glacier_wheel_index; #endif - if (vec_len (callback_vector) >= tw->max_expirations) + ticks_processed++; + + if (expirations_processed + vec_len (callback_vector) >= + tw->max_expirations) break; } if (callback_vector_arg == 0) tw->expired_timer_handles = callback_vector; - tw->last_run_time += i * tw->timer_interval; + /* Keep elapsed time as backlog while active timers remain. */ + tw->last_run_time += ticks_processed * tw->timer_interval; + tw->next_run_time = tw->last_run_time + tw->timer_interval; return callback_vector; } From eea31065887fb59198bc4159dc96422686e31e59 Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Thu, 23 Jul 2026 02:10:05 +0800 Subject: [PATCH 37/85] dpdk: switch to shared library build to include hinic PMD --- build/external/packages/dpdk.mk | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/build/external/packages/dpdk.mk b/build/external/packages/dpdk.mk index 2f0f8ea2788d..256dc61a1130 100644 --- a/build/external/packages/dpdk.mk +++ b/build/external/packages/dpdk.mk @@ -179,7 +179,7 @@ fi endef DPDK_MESON_ARGS = \ - --default-library static \ + --default-library shared \ --libdir lib \ --prefix $(dpdk_install_dir) \ -Dtests=false \ @@ -231,8 +231,7 @@ define dpdk_install_cmds source ../dpdk-meson-venv/bin/activate && \ meson install && \ cd $(dpdk_install_dir)/lib && \ - echo "GROUP ( $$(ls librte*.a ) )" > libdpdk.a && \ - rm -rf librte*.so librte*.so.* dpdk/*/librte*.so dpdk/*/librte*.so.* && \ + echo "GROUP ( $$(ls librte*.a 2>/dev/null) )" > libdpdk.a && \ deactivate endef From 0069965b0551b14688bcb1830110256d202ec655 Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Thu, 23 Jul 2026 02:19:51 +0800 Subject: [PATCH 38/85] Revert "dpdk: switch to shared library build to include hinic PMD" This reverts commit eea31065887fb59198bc4159dc96422686e31e59. --- build/external/packages/dpdk.mk | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/build/external/packages/dpdk.mk b/build/external/packages/dpdk.mk index 256dc61a1130..2f0f8ea2788d 100644 --- a/build/external/packages/dpdk.mk +++ b/build/external/packages/dpdk.mk @@ -179,7 +179,7 @@ fi endef DPDK_MESON_ARGS = \ - --default-library shared \ + --default-library static \ --libdir lib \ --prefix $(dpdk_install_dir) \ -Dtests=false \ @@ -231,7 +231,8 @@ define dpdk_install_cmds source ../dpdk-meson-venv/bin/activate && \ meson install && \ cd $(dpdk_install_dir)/lib && \ - echo "GROUP ( $$(ls librte*.a 2>/dev/null) )" > libdpdk.a && \ + echo "GROUP ( $$(ls librte*.a ) )" > libdpdk.a && \ + rm -rf librte*.so librte*.so.* dpdk/*/librte*.so dpdk/*/librte*.so.* && \ deactivate endef From 9bff4c9c729fb2865b027997795bc12768bbe79f Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Thu, 23 Jul 2026 02:54:24 +0800 Subject: [PATCH 39/85] dpdk: force-link hinic PMD with static DPDK Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/plugins/dpdk/CMakeLists.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/plugins/dpdk/CMakeLists.txt b/src/plugins/dpdk/CMakeLists.txt index 1a81e04b9192..15e0ff34138d 100644 --- a/src/plugins/dpdk/CMakeLists.txt +++ b/src/plugins/dpdk/CMakeLists.txt @@ -111,6 +111,11 @@ else() string_append(DPDK_LINK_FLAGS "-Wl,--whole-archive,${IBVERBS_LIB},${MLX5_LIB},${MLX4_LIB} -Wl,--no-whole-archive,--exclude-libs,ALL") endif() endif() + + if(DPDK_RTE_NET_HINIC AND NOT DPDK_IS_SHARED_LIB) + vpp_plugin_find_library(dpdk HINIC_LIB "librte_net_hinic.a") + string_append(DPDK_LINK_FLAGS "-Wl,--whole-archive,${HINIC_LIB},--no-whole-archive,--exclude-libs,ALL") + endif() endif() ############################################################################## From 3077b7c717cce9832c16114d609c9d342226b788 Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Thu, 23 Jul 2026 02:54:24 +0800 Subject: [PATCH 40/85] github: cache Trixie apt archives Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .github/workflows/build-deb-trixie.yml | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build-deb-trixie.yml b/.github/workflows/build-deb-trixie.yml index dc8721aace03..e71233399114 100644 --- a/.github/workflows/build-deb-trixie.yml +++ b/.github/workflows/build-deb-trixie.yml @@ -25,6 +25,17 @@ jobs: CCACHE_COMPRESS: "true" CCACHE_MAXSIZE: 2G steps: + - name: Disable APT cache cleanup + run: rm -f /etc/apt/apt.conf.d/docker-clean + + - name: Restore apt archive cache + uses: actions/cache/restore@v4 + with: + path: /var/cache/apt/archives + key: apt-debian13-${{ runner.arch }}-${{ github.run_id }} + restore-keys: | + apt-debian13-${{ runner.arch }}- + - name: Install base tools run: | apt-get update -qq @@ -42,24 +53,17 @@ jobs: git config --global --add safe.directory "$PWD" echo "short_sha=$(git rev-parse --short=7 HEAD)" >> $GITHUB_OUTPUT - - name: Restore apt cache - uses: actions/cache/restore@v4 - with: - path: /var/cache/apt - key: apt-debian13-${{ runner.arch }} - restore-keys: | - apt-debian13- - - name: Install VPP build dependencies run: | sed -i '/^\s*git config commit\.template/s/^\(\s*\)/\1-/' Makefile make UNATTENDED=yes install-dep - - name: Save apt cache + - name: Save apt archive cache + if: always() uses: actions/cache/save@v4 with: - path: /var/cache/apt - key: apt-debian13-${{ runner.arch }} + path: /var/cache/apt/archives + key: apt-debian13-${{ runner.arch }}-${{ github.run_id }} - name: Restore ccache uses: actions/cache/restore@v4 From 51842917a053ff716a7de750efa15a199676dee7 Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Thu, 23 Jul 2026 03:15:20 +0800 Subject: [PATCH 41/85] dpdk: avoid duplicate hinic PMD linkage Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/plugins/dpdk/CMakeLists.txt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/plugins/dpdk/CMakeLists.txt b/src/plugins/dpdk/CMakeLists.txt index 15e0ff34138d..5c48bb933860 100644 --- a/src/plugins/dpdk/CMakeLists.txt +++ b/src/plugins/dpdk/CMakeLists.txt @@ -112,10 +112,6 @@ else() endif() endif() - if(DPDK_RTE_NET_HINIC AND NOT DPDK_IS_SHARED_LIB) - vpp_plugin_find_library(dpdk HINIC_LIB "librte_net_hinic.a") - string_append(DPDK_LINK_FLAGS "-Wl,--whole-archive,${HINIC_LIB},--no-whole-archive,--exclude-libs,ALL") - endif() endif() ############################################################################## From d4a2599afa39aad04b43882667b5a2254e2800dc Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Thu, 23 Jul 2026 03:35:17 +0800 Subject: [PATCH 42/85] dpdk: allow Huawei Hi1822 devices Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/plugins/dpdk/device/init.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/plugins/dpdk/device/init.c b/src/plugins/dpdk/device/init.c index 6a9313107a81..9aa68c5d6c73 100644 --- a/src/plugins/dpdk/device/init.c +++ b/src/plugins/dpdk/device/init.c @@ -836,6 +836,9 @@ dpdk_bind_devices_to_uio (dpdk_config_main_t * conf) /* Google vNIC */ else if (d->vendor_id == 0x1ae0 && d->device_id == 0x0042) ; + /* Huawei Hi1822 */ + else if (d->vendor_id == 0x19e5 && d->device_id == 0x1822) + ; else { dpdk_log_warn ("Unsupported PCI device 0x%04x:0x%04x found " From dcbc5dc40704e29c7173f5587e08a0fc9ea99c03 Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Wed, 2 Sep 2026 18:34:58 +0800 Subject: [PATCH 43/85] gitignore agent workspace metadata --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 36115346288c..e1ae5228f16b 100644 --- a/.gitignore +++ b/.gitignore @@ -188,3 +188,7 @@ calicovppversion # log file archives /archives + +# Agent workspace metadata +/.omo/ +/.sisyphus/ From 88053b6f9b1e6b46cb314e01b3b9ca94ee5a87e5 Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Thu, 3 Sep 2026 02:03:10 +0800 Subject: [PATCH 44/85] gre: harden tunnel lifecycle --- src/plugins/gre/gre.c | 12 ++++++ src/plugins/gre/interface.c | 9 +++- src/plugins/gre/node.c | 2 +- test/test_gre.py | 54 +++++++++++++++++++++++ test/test_span.py | 85 +++++++++++++++++++++++++++++++++++++ 5 files changed, 159 insertions(+), 3 deletions(-) diff --git a/src/plugins/gre/gre.c b/src/plugins/gre/gre.c index dcb44010aca6..6d5c22742353 100644 --- a/src/plugins/gre/gre.c +++ b/src/plugins/gre/gre.c @@ -407,6 +407,12 @@ gre_update_adj (vnet_main_t *vnm, u32 sw_if_index, adj_index_t ai) adj_flags_t af; u32 ti; + if ((vec_len (gm->tunnel_index_by_sw_if_index) <= sw_if_index) || + (~0 == gm->tunnel_index_by_sw_if_index[sw_if_index]) || + pool_is_free_index (gm->tunnels, + gm->tunnel_index_by_sw_if_index[sw_if_index])) + return; + ti = gm->tunnel_index_by_sw_if_index[sw_if_index]; t = pool_elt_at_index (gm->tunnels, ti); af = ADJ_FLAG_NONE; @@ -479,6 +485,12 @@ mgre_update_adj (vnet_main_t *vnm, u32 sw_if_index, adj_index_t ai) gre_tunnel_t *t; u32 ti; + if ((vec_len (gm->tunnel_index_by_sw_if_index) <= sw_if_index) || + (~0 == gm->tunnel_index_by_sw_if_index[sw_if_index]) || + pool_is_free_index (gm->tunnels, + gm->tunnel_index_by_sw_if_index[sw_if_index])) + return; + adj = adj_get (ai); ti = gm->tunnel_index_by_sw_if_index[sw_if_index]; t = pool_elt_at_index (gm->tunnels, ti); diff --git a/src/plugins/gre/interface.c b/src/plugins/gre/interface.c index 1d2dfba61751..35ce18e64674 100644 --- a/src/plugins/gre/interface.c +++ b/src/plugins/gre/interface.c @@ -483,11 +483,13 @@ vnet_gre_tunnel_add (vnet_gre_tunnel_add_del_args_t *a, u32 outer_fib_index, { gre_sn_key_t skey; gre_sn_t *gre_sn; + uword *p; gre_mk_sn_key (t, &skey); - gre_sn = (gre_sn_t *) hash_get_mem (gm->seq_num_by_key, &skey); - if (gre_sn != NULL) + p = hash_get_mem (gm->seq_num_by_key, &skey); + if (p != NULL) { + gre_sn = (gre_sn_t *) p[0]; gre_sn->ref_count++; t->gre_sn = gre_sn; } @@ -594,6 +596,9 @@ vnet_gre_tunnel_add_del (vnet_gre_tunnel_add_del_args_t *a, u32 *sw_if_indexp) if (a->session_id > GTK_SESSION_ID_MAX) return VNET_API_ERROR_INVALID_SESSION_ID; + if (a->is_add && a->session_id != 0 && a->type != GRE_TUNNEL_TYPE_ERSPAN) + return VNET_API_ERROR_INVALID_SESSION_ID; + if (a->mode == TUNNEL_MODE_MP && !ip46_address_is_zero (&a->dst)) return (VNET_API_ERROR_INVALID_DST_ADDRESS); diff --git a/src/plugins/gre/node.c b/src/plugins/gre/node.c index 80926276913b..04cb90d0d0a5 100644 --- a/src/plugins/gre/node.c +++ b/src/plugins/gre/node.c @@ -450,7 +450,7 @@ gre_input (vlib_main_t *vm, vlib_node_runtime_t *node, vlib_frame_t *frame, vlib_node_increment_counter ( vm, is_ipv6 ? gre6_input_node.index : gre4_input_node.index, - GRE_ERROR_PKTS_DECAP, n_left_from); + GRE_ERROR_PKTS_DECAP, frame->n_vectors); return frame->n_vectors; } diff --git a/test/test_gre.py b/test/test_gre.py index 4ea52701ba88..e52ec3fe9cfc 100644 --- a/test/test_gre.py +++ b/test/test_gre.py @@ -645,8 +645,15 @@ def test_gre(self): self.pg0.local_ip4, self.pg0.remote_ip4, ) + decap_counter_before = self.statistics.get_err_counter( + "/err/gre4-input/GRE input packets decapsulated" + ) rx = self.send_and_expect(self.pg0, tx, self.pg0) self.verify_decapped_4o4(self.pg0, rx, tx) + decap_counter_after = self.statistics.get_err_counter( + "/err/gre4-input/GRE input packets decapsulated" + ) + self.assertEqual(decap_counter_after, decap_counter_before + len(tx)) # # Send tunneled packets that do not match the tunnel's src @@ -843,6 +850,53 @@ def test_gre_with_key(self): route_via_tun.remove_vpp_config() gre_if.remove_vpp_config() + def test_non_erspan_session_id_is_rejected(self): + tunnel_type = VppEnum.vl_api_gre_tunnel_type_t.GRE_API_TUNNEL_TYPE_L3 + tunnel_mode = VppEnum.vl_api_tunnel_mode_t.TUNNEL_API_MODE_P2P + + with self.vapi.assert_negative_api_retval(): + self.vapi.gre_tunnel_add_del_v2( + is_add=1, + tunnel={ + "src": self.pg0.local_ip4, + "dst": "1.1.1.2", + "outer_table_id": 0, + "instance": 0xFFFFFFFF, + "type": tunnel_type, + "mode": tunnel_mode, + "flags": 0, + "session_id": 1, + "key": 0, + }, + ) + + def test_teib_next_hop_family_matches_gre_underlay(self): + e = VppEnum.vl_api_tunnel_mode_t + gre_if = VppGreInterface( + self, + self.pg3.local_ip4, + "0.0.0.0", + mode=e.TUNNEL_API_MODE_MP, + ) + gre_if.add_vpp_config() + gre_if.admin_up() + gre_if.config_ip4() + + with self.vapi.assert_negative_api_retval(): + self.vapi.teib_entry_add_del( + is_add=1, + entry={ + "nh_table_id": 0, + "sw_if_index": gre_if.sw_if_index, + "peer": self.pg3.remote_ip4, + "nh": self.pg2.remote_ip6, + }, + ) + + gre_if.unconfig_ip4() + gre_if.admin_down() + gre_if.remove_vpp_config() + def test_gre6(self): """GRE IPv6 tunnel Tests""" diff --git a/test/test_span.py b/test/test_span.py index e25e844eafa5..c657b3495f04 100644 --- a/test/test_span.py +++ b/test/test_span.py @@ -334,6 +334,91 @@ def decap(p): self.verify_capture(pg1_pkts, pg2_decaped) + def test_span_l2_rx_dst_two_gre_erspan_share_sequence(self): + """SPAN l2 rx mirror into two gre-erspan sharing a sequence object""" + + self.sub_if.admin_up() + + gre_if1 = VppGreInterface( + self, + self.pg2.local_ip4, + self.pg2.remote_ip4, + session=543, + type=(VppEnum.vl_api_gre_tunnel_type_t.GRE_API_TUNNEL_TYPE_ERSPAN), + ) + gre_if2 = VppGreInterface( + self, + self.pg2.local_ip4, + self.pg2.remote_ip4, + session=544, + type=(VppEnum.vl_api_gre_tunnel_type_t.GRE_API_TUNNEL_TYPE_ERSPAN), + ) + + gre_if1.add_vpp_config() + gre_if2.add_vpp_config() + gre_if1.admin_up() + gre_if2.admin_up() + self.bridge(gre_if1.sw_if_index) + self.bridge(gre_if2.sw_if_index) + self.xconnect(self.sub_if.sw_if_index, self.pg1.sw_if_index) + + pkts = self.create_stream(self.pg0, self.pg_if_packet_sizes, do_dot1=True)[:4] + self.pg0.add_stream(pkts) + self.vapi.sw_interface_span_enable_disable( + self.sub_if.sw_if_index, gre_if1.sw_if_index, is_l2=1 + ) + self.vapi.sw_interface_span_enable_disable( + self.sub_if.sw_if_index, gre_if2.sw_if_index, is_l2=1 + ) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + self.pg1.get_capture(len(pkts)) + mirrored = self.pg2.get_capture(2 * len(pkts)) + sessions = {packet[ERSPAN].session_id for packet in mirrored} + self.assertEqual(sessions, {543, 544}) + + sequence_numbers = [] + for packet in mirrored: + self.decap_erspan(packet, packet[ERSPAN].session_id) + gre_bytes = bytes(packet[GRE]) + self.assertEqual(int.from_bytes(gre_bytes[0:2], "big") & 0x1000, 0x1000) + sequence_numbers.append(int.from_bytes(gre_bytes[4:8], "big")) + + self.assertEqual( + sorted(sequence_numbers), + list(range(min(sequence_numbers), min(sequence_numbers) + len(mirrored))), + ) + + self.vapi.sw_interface_span_enable_disable( + self.sub_if.sw_if_index, gre_if1.sw_if_index, state=0, is_l2=1 + ) + self.bridge(gre_if1.sw_if_index, is_add=0) + gre_if1.remove_vpp_config() + + pkts = self.create_stream(self.pg0, self.pg_if_packet_sizes, do_dot1=True)[:2] + self.pg0.add_stream(pkts) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + self.pg1.get_capture(len(pkts)) + mirrored = self.pg2.get_capture(len(pkts)) + self.assertEqual({packet[ERSPAN].session_id for packet in mirrored}, {544}) + remaining_sequence_numbers = [] + for packet in mirrored: + self.decap_erspan(packet, 544) + gre_bytes = bytes(packet[GRE]) + remaining_sequence_numbers.append(int.from_bytes(gre_bytes[4:8], "big")) + self.assertTrue(min(remaining_sequence_numbers) > max(sequence_numbers)) + + self.vapi.sw_interface_span_enable_disable( + self.sub_if.sw_if_index, gre_if2.sw_if_index, state=0, is_l2=1 + ) + self.bridge(gre_if2.sw_if_index, is_add=0) + gre_if2.remove_vpp_config() + self.xconnect(self.sub_if.sw_if_index, self.pg1.sw_if_index, is_add=0) + def test_span_l2_rx_dst_gre_subif_vtr(self): """SPAN l2 rx mirror into gre-subif+vtr""" From 9585f21cd7b14e2398078c4227c53ca5f1f48b17 Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Thu, 3 Sep 2026 02:03:19 +0800 Subject: [PATCH 45/85] linux-cp: recover from netlink errors --- src/plugins/linux-cp/lcp_nl.c | 203 +++++++++++++++++----- src/vnet/devices/netlink.c | 23 ++- test/test_linux_cp.py | 318 +++++++++++++++++++++++++++++++++- 3 files changed, 493 insertions(+), 51 deletions(-) diff --git a/src/plugins/linux-cp/lcp_nl.c b/src/plugins/linux-cp/lcp_nl.c index 56cdd5ec17c3..0b87f5942a13 100644 --- a/src/plugins/linux-cp/lcp_nl.c +++ b/src/plugins/linux-cp/lcp_nl.c @@ -181,9 +181,9 @@ nl_register_vft (const nl_vft_t *nv) #define NL_INFO(...) vlib_log_notice (nl_main.nl_logger, __VA_ARGS__); #define NL_ERROR(...) vlib_log_err (nl_main.nl_logger, __VA_ARGS__); -static void lcp_nl_open_socket (void); +static int lcp_nl_open_socket (void); static void lcp_nl_close_socket (void); -static void lcp_nl_open_sync_socket (nl_sock_type_t sock_type); +static int lcp_nl_open_sync_socket (nl_sock_type_t sock_type); static void lcp_nl_close_sync_socket (nl_sock_type_t sock_type); static void @@ -395,6 +395,9 @@ lcp_nl_route_send_dump_req (nl_sock_type_t sock_type, int msg_type) .rtgen_family = AF_UNSPEC, }; + if (!sk_route) + return -NLE_BAD_SOCK; + err = nl_send_simple (sk_route, msg_type, NLM_F_DUMP, &rt_hdr, sizeof (rt_hdr)); @@ -415,7 +418,10 @@ lcp_nl_route_dump_cb (struct nl_msg *msg, void *arg) int err; if ((err = nl_msg_parse (msg, nl_route_dispatch, NULL)) < 0) - NL_ERROR ("Unable to parse object: %s", nl_geterror (err)); + { + NL_ERROR ("Unable to parse object: %s", nl_geterror (err)); + return err; + } return NL_OK; } @@ -435,6 +441,9 @@ lcp_nl_recv_dump_replies (nl_sock_type_t sock_type, int msg_limit, int done = 0; int n_msgs = 0; + if (!sk_route) + return -NLE_BAD_SOCK; + lcp_set_netlink_processing_active (1); continue_reading: @@ -505,12 +514,20 @@ lcp_nl_recv_dump_replies (nl_sock_type_t sock_type, int msg_limit, */ else { - lcp_nl_route_dump_cb (msg, NULL); + err = lcp_nl_route_dump_cb (msg, NULL); + if (err < 0) + goto out; } hdr = nlmsg_next (hdr, &n_bytes); } + if (n_bytes != 0) + { + err = -NLE_MSG_TRUNC; + goto out; + } + nlmsg_free (msg); free (buf); msg = NULL; @@ -604,7 +621,9 @@ nl_route_process (vlib_main_t *vm, vlib_node_runtime_t *node, * If send a dump request while the previous one is in progress, * the request will fail and EBUSY returned */ -#define _(stype, mtype, tname, fn) lcp_nl_open_sync_socket (stype); +#define _(stype, mtype, tname, fn) \ + if (lcp_nl_open_sync_socket (stype) < 0) \ + goto sync_later; foreach_sock_type #undef _ @@ -614,19 +633,25 @@ nl_route_process (vlib_main_t *vm, vlib_node_runtime_t *node, * the moment. Once all the dump replies are processed, the * notifications will be processed */ - lcp_nl_open_socket (); + if (lcp_nl_open_socket () < 0) + goto sync_later; /* Request the current entry set from the kernel for every data type * of interest. Thus requesting a snapshot of the current routing * state that the kernel will make and then reply with */ -#define _(stype, mtype, tname, fn) lcp_nl_route_send_dump_req (stype, mtype); +#define _(stype, mtype, tname, fn) \ + if (lcp_nl_route_send_dump_req (stype, mtype) < 0) \ + goto sync_later; foreach_sock_type #undef _ +#define _(stype, mtype, tname, fn) nl_##fn##_sync_begin (); + foreach_sock_type +#undef _ + /* Process all the dump replies */ #define _(stype, mtype, tname, fn) \ - nl_##fn##_sync_begin (); \ is_done = 0; \ do \ { \ @@ -637,12 +662,12 @@ nl_route_process (vlib_main_t *vm, vlib_node_runtime_t *node, NL_ERROR ("Error receiving dump replies of type " tname \ ": %s (%d)", \ nl_geterror (n_msgs), n_msgs); \ - break; \ + goto sync_later; \ } \ else if (n_msgs == 0) \ { \ NL_ERROR ("EOF while receiving dump replies of type " tname); \ - break; \ + goto sync_later; \ } \ else \ NL_INFO ("Processed %u dump replies of type " tname, n_msgs); \ @@ -655,7 +680,7 @@ nl_route_process (vlib_main_t *vm, vlib_node_runtime_t *node, * incomplete \ */ \ vlib_process_wait_for_event_or_clock (vm, \ - nm->sync_batch_delay_ms * 1e-3); \ + nm->sync_batch_delay_ms * 1e-3); \ event_type = vlib_process_get_events (vm, &event_data); \ vec_reset_length (event_data); \ \ @@ -665,14 +690,17 @@ nl_route_process (vlib_main_t *vm, vlib_node_runtime_t *node, if (event_type == NL_EVENT_ERR) \ goto sync_later; \ } \ - while (!is_done); \ - nl_##fn##_sync_end (); + while (!is_done); - foreach_sock_type + foreach_sock_type +#undef _ + +#define _(stype, mtype, tname, fn) nl_##fn##_sync_end (); + foreach_sock_type #undef _ - /* Start processing notifications */ - nm->nl_status = NL_STATUS_NOTIF_PROC; + /* Start processing notifications */ + nm->nl_status = NL_STATUS_NOTIF_PROC; /* Trigger messages processing if there are notifications received * during synchronization @@ -751,7 +779,12 @@ lcp_nl_pair_add_cb (lcp_itf_pair_t *pair) if (!nm->sk_route) { NL_INFO ("pair_add_cb: Opening netlink socket, LCP pairs %u", lcp_itf_num_pairs ()); - lcp_nl_open_socket (); + if (lcp_nl_open_socket () < 0) + { + nm->nl_status = NL_STATUS_SYNC; + vlib_process_signal_event (vlib_get_main (), nl_route_process_node.index, + NL_EVENT_ERR, 0); + } } else if (!lcp_get_netlink_processing_active ()) lcp_nl_drain_messages (); @@ -886,11 +919,13 @@ lcp_nl_close_socket (void) } } -static void +static int lcp_nl_open_socket (void) { nl_main_t *nm = &nl_main; - int dest_ns_fd, curr_ns_fd; + int dest_ns_fd, curr_ns_fd = -1; + int entered_dest_ns = 0; + int err; /* Allocate a new socket for both routes and acls * Notifications do not use sequence numbers, disable sequence number @@ -899,31 +934,56 @@ lcp_nl_open_socket (void) * received */ nm->sk_route = nl_socket_alloc (); + if (!nm->sk_route) + return -NLE_NOMEM; nl_socket_disable_seq_check (nm->sk_route); dest_ns_fd = lcp_get_default_ns_fd (); - if (dest_ns_fd) + if (dest_ns_fd > 0) { - curr_ns_fd = open ("/proc/self/ns/net", O_RDONLY); - setns (dest_ns_fd, CLONE_NEWNET); + curr_ns_fd = clib_netns_open (NULL /* self */); + if (curr_ns_fd < 0) + { + err = -NLE_FAILURE; + goto error; + } + if (clib_setns (dest_ns_fd) < 0) + { + err = -NLE_FAILURE; + goto error; + } + entered_dest_ns = 1; } - nl_connect (nm->sk_route, NETLINK_ROUTE); + err = nl_connect (nm->sk_route, NETLINK_ROUTE); + if (err < 0) + goto error; - if (dest_ns_fd && curr_ns_fd >= 0) + if (entered_dest_ns) + { + if (clib_setns (curr_ns_fd) < 0) + { + err = -NLE_FAILURE; + goto error; + } + entered_dest_ns = 0; + } + if (curr_ns_fd >= 0) { - setns (curr_ns_fd, CLONE_NEWNET); close (curr_ns_fd); + curr_ns_fd = -1; } /* Subscribe to all the 'routing' notifications on the route socket */ - nl_socket_add_memberships (nm->sk_route, RTNLGRP_LINK, RTNLGRP_IPV6_IFADDR, - RTNLGRP_IPV4_IFADDR, RTNLGRP_IPV4_ROUTE, - RTNLGRP_IPV6_ROUTE, RTNLGRP_NEIGH, RTNLGRP_NOTIFY, + err = nl_socket_add_memberships (nm->sk_route, RTNLGRP_LINK, RTNLGRP_IPV6_IFADDR, + RTNLGRP_IPV4_IFADDR, RTNLGRP_IPV4_ROUTE, RTNLGRP_IPV6_ROUTE, + RTNLGRP_NEIGH, RTNLGRP_NOTIFY, #ifdef RTNLGRP_MPLS_ROUTE /* not defined on CentOS/RHEL 7 */ - RTNLGRP_MPLS_ROUTE, + RTNLGRP_MPLS_ROUTE, #endif - RTNLGRP_IPV4_RULE, RTNLGRP_IPV6_RULE, 0); + RTNLGRP_IPV4_RULE, RTNLGRP_IPV6_RULE, 0); + if (err < 0) + goto error; /* Set socket in nonblocking mode and increase buffer sizes */ nl_socket_set_nonblocking (nm->sk_route); @@ -955,13 +1015,34 @@ lcp_nl_open_socket (void) nl_socket_modify_cb (nm->sk_route, NL_CB_VALID, NL_CB_CUSTOM, nl_route_cb, NULL); NL_INFO ("Opened netlink socket %d", nl_socket_get_fd (nm->sk_route)); + return 0; + +error: + if (entered_dest_ns) + { + if (clib_setns (curr_ns_fd) < 0) + NL_ERROR ("Cannot set previous ns"); + else + entered_dest_ns = 0; + } + if (curr_ns_fd >= 0) + { + close (curr_ns_fd); + curr_ns_fd = -1; + } + NL_ERROR ("Unable to open netlink socket: %s", nl_geterror (err)); + nl_socket_free (nm->sk_route); + nm->sk_route = NULL; + return err; } -static void +static int lcp_nl_open_sync_socket (nl_sock_type_t sock_type) { nl_main_t *nm = &nl_main; - int dest_ns_fd, curr_ns_fd; + int dest_ns_fd, curr_ns_fd = -1; + int entered_dest_ns = 0; + int err; struct nl_sock *sk_route; /* Allocate a new blocking socket for routes that will be used for dump @@ -971,33 +1052,67 @@ lcp_nl_open_sync_socket (nl_sock_type_t sock_type) */ nm->sk_route_sync[sock_type] = sk_route = nl_socket_alloc (); + if (!sk_route) + return -NLE_NOMEM; dest_ns_fd = lcp_get_default_ns_fd (); if (dest_ns_fd > 0) { curr_ns_fd = clib_netns_open (NULL /* self */); - if (clib_setns (dest_ns_fd) == -1) - NL_ERROR ("Cannot set destination ns"); + if (curr_ns_fd < 0) + { + err = -NLE_FAILURE; + goto error; + } + if (clib_setns (dest_ns_fd) < 0) + { + err = -NLE_FAILURE; + goto error; + } + entered_dest_ns = 1; } - nl_connect (sk_route, NETLINK_ROUTE); + err = nl_connect (sk_route, NETLINK_ROUTE); + if (err < 0) + goto error; - if (dest_ns_fd > 0) + if (entered_dest_ns) { - if (curr_ns_fd == -1) - { - NL_ERROR ("No previous ns to set"); - } - else + if (clib_setns (curr_ns_fd) < 0) { - if (clib_setns (curr_ns_fd) == -1) - NL_ERROR ("Cannot set previous ns"); - close (curr_ns_fd); + err = -NLE_FAILURE; + goto error; } + entered_dest_ns = 0; + } + if (curr_ns_fd >= 0) + { + close (curr_ns_fd); + curr_ns_fd = -1; } NL_INFO ("Opened netlink synchronization socket %d of type %d", nl_socket_get_fd (sk_route), sock_type); + return 0; + +error: + if (entered_dest_ns) + { + if (clib_setns (curr_ns_fd) < 0) + NL_ERROR ("Cannot set previous ns"); + else + entered_dest_ns = 0; + } + if (curr_ns_fd >= 0) + { + close (curr_ns_fd); + curr_ns_fd = -1; + } + nl_socket_free (sk_route); + nm->sk_route_sync[sock_type] = NULL; + NL_ERROR ("Unable to open netlink synchronization socket of type %d: %s", sock_type, + nl_geterror (err)); + return err; } static void diff --git a/src/vnet/devices/netlink.c b/src/vnet/devices/netlink.c index 59d4aa2d5233..90fd4c08680f 100644 --- a/src/vnet/devices/netlink.c +++ b/src/vnet/devices/netlink.c @@ -92,7 +92,8 @@ vnet_netlink_msg_send (vnet_netlink_msg_t *m, vnet_netlink_msg_t **replies) { struct nlmsgerr *e = (struct nlmsgerr *) NLMSG_DATA (nh); if (e->error) - err = clib_error_return (0, "netlink error %d", e->error); + err = clib_error_return_code (0, -e->error, 0, "netlink error %d", + e->error); goto done; } @@ -323,7 +324,7 @@ vnet_netlink_add_ip4_addr (int ifindex, void *addr, int pfx_len) vnet_netlink_msg_add_rtattr (&m, IFA_ADDRESS, addr, 4); err = vnet_netlink_msg_send (&m, NULL); if (err) - err = clib_error_return (0, "add ip4 addr %U", format_clib_error, err); + err = clib_error_return (err, "add ip4 addr"); return err; } @@ -346,7 +347,7 @@ vnet_netlink_add_ip6_addr (int ifindex, void *addr, int pfx_len) vnet_netlink_msg_add_rtattr (&m, IFA_ADDRESS, addr, 16); err = vnet_netlink_msg_send (&m, NULL); if (err) - err = clib_error_return (0, "add ip6 addr %U", format_clib_error, err); + err = clib_error_return (err, "add ip6 addr"); return err; } @@ -417,8 +418,14 @@ vnet_netlink_del_ip4_addr (int ifindex, void *addr, int pfx_len) vnet_netlink_msg_add_rtattr (&m, IFA_LOCAL, addr, 4); vnet_netlink_msg_add_rtattr (&m, IFA_ADDRESS, addr, 4); err = vnet_netlink_msg_send (&m, NULL); + if (err && clib_error_get_code (err) == EADDRNOTAVAIL && + !(err->flags & CLIB_ERROR_ERRNO_VALID)) + { + clib_error_free (err); + return 0; + } if (err) - err = clib_error_return (0, "del ip4 addr %U", format_clib_error, err); + err = clib_error_return (err, "del ip4 addr"); return err; } @@ -439,7 +446,13 @@ vnet_netlink_del_ip6_addr (int ifindex, void *addr, int pfx_len) vnet_netlink_msg_add_rtattr (&m, IFA_LOCAL, addr, 16); vnet_netlink_msg_add_rtattr (&m, IFA_ADDRESS, addr, 16); err = vnet_netlink_msg_send (&m, NULL); + if (err && clib_error_get_code (err) == EADDRNOTAVAIL && + !(err->flags & CLIB_ERROR_ERRNO_VALID)) + { + clib_error_free (err); + return 0; + } if (err) - err = clib_error_return (0, "del ip6 addr %U", format_clib_error, err); + err = clib_error_return (err, "del ip6 addr"); return err; } diff --git a/test/test_linux_cp.py b/test/test_linux_cp.py index 8e24e05c908d..79fec3b71db7 100644 --- a/test/test_linux_cp.py +++ b/test/test_linux_cp.py @@ -1,12 +1,14 @@ #!/usr/bin/env python3 import unittest +import os import socket +import subprocess from ipaddress import ip_address, ip_interface from scapy.layers.inet import IP, UDP from scapy.layers.inet6 import IPv6, Raw -from scapy.layers.l2 import Ether, ARP, Dot3, LLC +from scapy.layers.l2 import Ether, ARP, Dot3, LLC, GRE from scapy.contrib.lacp import LACP from scapy.contrib.lldp import ( LLDPDUChassisID, @@ -37,7 +39,8 @@ ) from test_ipsec_tun_if_esp import TemplateIpsecItf4 from config import config -from vpp_ip_route import FibPathType +from vpp_ip_route import FibPathType, VppRoutePath +from vpp_gre_interface import VppGreInterface from vpp_qemu_utils import ( add_namespace_route, add_namespace_multipath_route, @@ -966,6 +969,8 @@ def _prefix_tuple(prefix): class TestLinuxCPSync(TestLinuxCPNetNSBase): """Linux CP VPP-to-Linux State Sync""" + VNET_API_ERROR_ADDRESS_NOT_FOUND_FOR_INTERFACE = -60 + # Addresses assigned to loop0 in the IPv4/IPv6 address sync tests. loop0_prefixes_v4 = ["10.10.1.2/24", "10.10.2.2/24"] loop0_prefixes_v6 = ["2001:db8:1::2/64", "2001:db8:2::2/64"] @@ -995,6 +1000,13 @@ def _del_addr(self, sw_if_index, prefix): sw_if_index=sw_if_index, prefix=prefix, is_add=0 ) + def _vpp_has_ipv4_address(self, sw_if_index, prefix): + """Return whether VPP reports an IPv4 address on an interface.""" + return any( + str(address.prefix) == prefix + for address in self.vapi.ip_address_dump(sw_if_index, is_ipv6=False) + ) + def test_lcp_sync_admin_state(self): """VPP admin state changes sync to Linux""" # Loopbacks default to admin-down, so the host side should already @@ -1118,6 +1130,91 @@ def test_lcp_sync_ipv4_addr(self): # Cleanup loop0.admin_down() + def test_lcp_sync_ipv4_addr_delete_already_absent(self): + """Deleting a VPP address after Linux removed it keeps state converged""" + loop0 = self.lo_interfaces[0] + prefix = "10.40.1.2/24" + prefix_tuple = _prefix_tuple(prefix) + + try: + loop0.admin_up() + self.poll_for( + "hloop0 up", lambda: is_interface_up(self.ns_name, "hloop0"), True + ) + + self._add_addr(loop0.sw_if_index, prefix) + self.poll_for( + f"hloop0 has {prefix}", + lambda: prefix_tuple + in get_interface_addresses(self.ns_name, "hloop0", family="inet"), + True, + ) + + del_namespace_address(self.ns_name, "hloop0", prefix) + self.poll_for( + f"hloop0 no longer has {prefix}", + lambda: prefix_tuple + not in get_interface_addresses(self.ns_name, "hloop0", family="inet"), + True, + ) + # The Linux->VPP sync may already have removed the address. + with self.vapi.assert_known_api_retval( + [0, self.VNET_API_ERROR_ADDRESS_NOT_FOUND_FOR_INTERFACE] + ): + self._del_addr(loop0.sw_if_index, prefix) + self.poll_for( + f"VPP no longer has {prefix}", + lambda: not self._vpp_has_ipv4_address(loop0.sw_if_index, prefix), + True, + ) + self.assertNotIn( + prefix_tuple, + get_interface_addresses(self.ns_name, "hloop0", family="inet"), + ) + finally: + if self._vpp_has_ipv4_address(loop0.sw_if_index, prefix): + self._del_addr(loop0.sw_if_index, prefix) + loop0.admin_down() + + def test_lcp_sync_ipv4_addr_external_delete_preserves_other_state(self): + """Removing one Linux address does not sweep another valid VPP address""" + loop0 = self.lo_interfaces[0] + prefixes = ("10.40.3.2/24", "10.40.4.2/24") + prefix_tuples = tuple(_prefix_tuple(prefix) for prefix in prefixes) + + try: + loop0.admin_up() + self.poll_for( + "hloop0 up", lambda: is_interface_up(self.ns_name, "hloop0"), True + ) + for prefix in prefixes: + self._add_addr(loop0.sw_if_index, prefix) + self.poll_for( + "hloop0 has both ipv4 addresses", + lambda: sorted( + get_interface_addresses(self.ns_name, "hloop0", family="inet") + ), + sorted(prefix_tuples), + ) + + del_namespace_address(self.ns_name, "hloop0", prefixes[0]) + self.poll_for( + f"VPP removed {prefixes[0]}", + lambda: not self._vpp_has_ipv4_address(loop0.sw_if_index, prefixes[0]), + True, + ) + + self.assertTrue(self._vpp_has_ipv4_address(loop0.sw_if_index, prefixes[1])) + self.assertEqual( + sorted(get_interface_addresses(self.ns_name, "hloop0", family="inet")), + [prefix_tuples[1]], + ) + finally: + for prefix in prefixes: + if self._vpp_has_ipv4_address(loop0.sw_if_index, prefix): + self._del_addr(loop0.sw_if_index, prefix) + loop0.admin_down() + def test_lcp_sync_ipv6_addr(self): """VPP IPv6 address changes sync to Linux""" loop0 = self.lo_interfaces[0] @@ -1503,6 +1600,223 @@ def test_pair_create_delete_tun(self): False, ) + @unittest.skipIf( + "linux-cp" in config.excluded_plugins, "Exclude linux-cp plugin tests" + ) + @unittest.skipIf("gre" in config.excluded_plugins, "Exclude GRE plugin tests") + def test_gre_lcp_ipv4_survives_pair_churn(self): + self.create_pg_interfaces(range(1)) + underlay = self.pg_interfaces[0] + gre_if = VppGreInterface(self, underlay.local_ip4, underlay.remote_ip4) + sender = None + capture_active = False + gre_created = False + route_created = False + pair_created = False + linux_route_present = False + ready_file = os.path.join(self.tempdir, "gre_lcp_sender_ready") + go_file = os.path.join(self.tempdir, "gre_lcp_sender_go") + inner_dst = "198.18.0.1" + + try: + underlay.admin_up() + underlay.config_ip4() + underlay.resolve_arp() + + gre_if.add_vpp_config() + gre_created = True + gre_if.admin_up() + self.vapi.cli( + f"lcp create {gre_if.name} host-if hgre0 netns {self.ns_name} tun" + ) + pair_created = True + self.poll_for( + "GRE LCP pair exists", + lambda: self._find_lcp_pair(gre_if.sw_if_index) is not None, + True, + ) + pair = self._find_lcp_pair(gre_if.sw_if_index) + self.assertIsNotNone(pair) + self.assertEqual(pair.host_if_type, 1, "GRE LCP pair should use TUN") + + gre_if.config_ip4() + route = VppIpRoute( + self, + inner_dst, + 32, + [VppRoutePath("0.0.0.0", gre_if.sw_if_index)], + ) + route.add_vpp_config() + route_created = True + expected_path = dict( + type=FibPathType.FIB_PATH_TYPE_NORMAL, + sw_if_index=gre_if.sw_if_index, + ) + self.verify_paths(f"{inner_dst}/32", expected_path) + expected_address = [_prefix_tuple(gre_if.local_ip4_prefix)] + self.poll_for( + "hgre0 has GRE IPv4 address", + lambda: get_interface_addresses(self.ns_name, "hgre0", family="inet"), + expected_address, + ) + add_namespace_route(self.ns_name, f"{inner_dst}/32", dev="hgre0") + linux_route_present = True + + sender_script = """ +import os +import sys +import time +from scapy.error import Scapy_Exception +from scapy.all import IP, Raw, UDP, send + +iface, source, destination, ready, go = sys.argv[1:] +def send_packet(sequence): + try: + send(IP(src=source, dst=destination) / UDP(sport=1234, dport=1234) / Raw(str(sequence).encode()), iface=iface, verbose=0) + except (OSError, Scapy_Exception, ValueError): + pass + +for sequence in range(20): + send_packet(sequence) +open(ready, "w").close() +sequence = 20 +while not os.path.exists(go): + send_packet(sequence) + sequence += 1 + time.sleep(0.01) +for sequence in range(100, 130): + send_packet(sequence) + time.sleep(0.01) +""" + underlay.enable_capture() + capture_active = True + sender = subprocess.Popen( + [ + "ip", + "netns", + "exec", + self.ns_name, + "python3", + "-c", + sender_script, + "hgre0", + gre_if.local_ip4, + inner_dst, + ready_file, + go_file, + ], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + self.poll_for( + "inner IPv4 sender is active", + lambda: os.path.exists(ready_file), + True, + ) + self.assertIsNone(self.vpp.poll(), "VPP must be alive before churn") + + gre_if.admin_down() + self.poll_for( + "hgre0 down after GRE admin_down", + lambda: is_interface_up(self.ns_name, "hgre0"), + False, + ) + gre_if.admin_up() + self.poll_for( + "hgre0 up after GRE admin_up", + lambda: is_interface_up(self.ns_name, "hgre0"), + True, + ) + + self.vapi.cli(f"lcp delete {gre_if.name}") + pair_created = False + self.poll_for( + "GRE LCP pair removed", + lambda: self._find_lcp_pair(gre_if.sw_if_index) is not None, + False, + ) + self.poll_for( + "hgre0 removed after pair delete", + lambda: interface_exists(self.ns_name, "hgre0"), + False, + ) + del_namespace_route(self.ns_name, f"{inner_dst}/32", check=False) + linux_route_present = False + self.assertIsNone(self.vpp.poll(), "VPP must be alive after pair delete") + + self.vapi.cli( + f"lcp create {gre_if.name} host-if hgre0 netns {self.ns_name} tun" + ) + pair_created = True + self.poll_for( + "GRE LCP pair recreated", + lambda: self._find_lcp_pair(gre_if.sw_if_index) is not None, + True, + ) + self.poll_for( + "hgre0 recreated", + lambda: interface_exists(self.ns_name, "hgre0"), + True, + ) + self.poll_for( + "hgre0 has GRE IPv4 address after recreation", + lambda: get_interface_addresses(self.ns_name, "hgre0", family="inet"), + expected_address, + ) + add_namespace_route(self.ns_name, f"{inner_dst}/32", dev="hgre0") + linux_route_present = True + self.verify_paths(f"{inner_dst}/32", expected_path) + self.assertTrue(gre_if.query_vpp_config()) + self.assertIn(gre_if.name, self.vapi.cli("show adj")) + self.assertIsNone( + self.vpp.poll(), "VPP must be alive after pair recreation" + ) + + with open(go_file, "w"): + pass + sender.wait(timeout=10) + self.assertEqual(sender.returncode, 0, "inner IPv4 sender failed") + + def _capture_inner_sequences(): + captured = underlay._get_capture() + if captured is None: + return [] + sequences = [] + for packet in captured: + if packet.haslayer(GRE): + inner = IP(bytes(packet[GRE].payload)) + if inner.dst == inner_dst and inner.haslayer(Raw): + sequences.append(int(bytes(inner[Raw].load))) + return sequences + + self.poll_for( + "capture has pre- and post-churn inner packets", + lambda: any(sequence < 20 for sequence in _capture_inner_sequences()) + and any(sequence >= 100 for sequence in _capture_inner_sequences()), + True, + timeout=10, + ) + capture_active = False + inner_sequences = _capture_inner_sequences() + self.assertTrue(any(sequence < 20 for sequence in inner_sequences)) + self.assertTrue(any(sequence >= 100 for sequence in inner_sequences)) + finally: + if sender is not None and sender.poll() is None: + sender.terminate() + sender.wait(timeout=5) + if capture_active: + underlay.disable_capture() + if pair_created: + self.vapi.cli(f"lcp delete {gre_if.name}") + if linux_route_present: + del_namespace_route(self.ns_name, f"{inner_dst}/32", check=False) + if route_created: + route.remove_vpp_config() + if gre_created: + gre_if.remove_vpp_config() + underlay.unconfig_ip4() + underlay.admin_down() + def test_pair_create_delete_tap(self): """Create and delete TAP LCP pair on pg interface""" self.create_pg_interfaces(range(1)) From e577dceb5f1a5e35dd0dc341647b4c81df15b062 Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Thu, 3 Sep 2026 02:03:28 +0800 Subject: [PATCH 46/85] teib: validate address families --- src/vnet/teib/teib.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/vnet/teib/teib.c b/src/vnet/teib/teib.c index 6fa3167b8503..84d765d1501f 100644 --- a/src/vnet/teib/teib.c +++ b/src/vnet/teib/teib.c @@ -189,6 +189,9 @@ teib_entry_add (u32 sw_if_index, u32 nh_fib_index, peer_fib_index; index_t tei; + if (ip_addr_version (nh) != ip_addr_version (peer)) + return (VNET_API_ERROR_INVALID_ADDRESS_FAMILY); + nh_proto = (AF_IP4 == ip_addr_version (nh) ? FIB_PROTOCOL_IP4 : FIB_PROTOCOL_IP6); From 3fb0ee613171c49cf3ce2de4f4746bbf81428d13 Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Thu, 3 Sep 2026 02:14:28 +0800 Subject: [PATCH 47/85] linux-cp: fix netlink error cleanup syntax NL_ERROR expands with a trailing semicolon, so an unbraced if (clib_setns (...) < 0) NL_ERROR (...); else ... becomes a syntax error under clang -Werror. Brace both branches in the error cleanup paths of lcp_nl_open_socket and lcp_nl_open_sync_socket. --- src/plugins/linux-cp/lcp_nl.c | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/plugins/linux-cp/lcp_nl.c b/src/plugins/linux-cp/lcp_nl.c index 0b87f5942a13..50a7e5e7ba45 100644 --- a/src/plugins/linux-cp/lcp_nl.c +++ b/src/plugins/linux-cp/lcp_nl.c @@ -1021,9 +1021,13 @@ lcp_nl_open_socket (void) if (entered_dest_ns) { if (clib_setns (curr_ns_fd) < 0) - NL_ERROR ("Cannot set previous ns"); + { + NL_ERROR ("Cannot set previous ns"); + } else - entered_dest_ns = 0; + { + entered_dest_ns = 0; + } } if (curr_ns_fd >= 0) { @@ -1099,9 +1103,13 @@ lcp_nl_open_sync_socket (nl_sock_type_t sock_type) if (entered_dest_ns) { if (clib_setns (curr_ns_fd) < 0) - NL_ERROR ("Cannot set previous ns"); + { + NL_ERROR ("Cannot set previous ns"); + } else - entered_dest_ns = 0; + { + entered_dest_ns = 0; + } } if (curr_ns_fd >= 0) { From 7a9ad8d325d5aef25eb0c73cc655b8ab2dfe28fd Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 15 Sep 2026 23:42:11 +0800 Subject: [PATCH 48/85] fib: guard attached export against bad fib index fib_table_get() indexes the per-protocol FIB pool without validating its argument, and fib_table_lookup_exact_match() dereferences the result. The exporter index passed to fib_attached_export_import() is derived from a route's resolving interface, which yields ~0 when the interface is not bound to a table or has already been removed. Such an index reached fib_table_lookup_exact_match() and the process faulted. Reject ~0 in fib_attached_export_import(), and validate the index in fib_table_lookup() and fib_table_lookup_exact_match() so that a bad index yields an empty lookup instead of a read through a wild pointer. fib_entry_post_flag_update_actions() no longer requests an export import when the index it derived is ~0. Type: fix --- src/vnet/fib/fib_attached_export.c | 10 ++++++ src/vnet/fib/fib_entry.c | 11 +++++- src/vnet/fib/fib_table.c | 55 +++++++++++++++++++++++++++--- 3 files changed, 71 insertions(+), 5 deletions(-) diff --git a/src/vnet/fib/fib_attached_export.c b/src/vnet/fib/fib_attached_export.c index 60d99f5748d4..480f0e34f8df 100644 --- a/src/vnet/fib/fib_attached_export.c +++ b/src/vnet/fib/fib_attached_export.c @@ -233,6 +233,16 @@ fib_attached_export_import (fib_entry_t *fib_entry, fib_ae_import_t *import; fib_node_index_t fei; + /* + * The exporter FIB index is derived from a route's resolving interface. + * Interfaces that are not bound to a table, and interfaces that have + * been deleted, yield ~0. fib_table_get() would then index the FIB pool + * out of range and fib_table_lookup_exact_match() would dereference the + * result. Nothing to export to, so do nothing. + */ + if (FIB_NODE_INDEX_INVALID == export_fib) + return; + /* * save index for later post-realloc retrieval */ diff --git a/src/vnet/fib/fib_entry.c b/src/vnet/fib/fib_entry.c index 688195a90010..f39aa0d81fdf 100644 --- a/src/vnet/fib/fib_entry.c +++ b/src/vnet/fib/fib_entry.c @@ -605,7 +605,16 @@ fib_entry_post_flag_update_actions (fib_entry_t *fib_entry, fib_entry_get_proto(fib_entry), sw_if_index); } - fib_attached_export_import(fib_entry, new_fib_index); + + /* + * The resolving interface may be unbound, or bound to a table that + * has since been removed. fib_table_get_index_for_sw_if_index() + * reports that as ~0. There is no exporter table to import from, so + * leave the entry alone; it will be revisited when the interface is + * bound to a table again. + */ + if (~0 != new_fib_index) + fib_attached_export_import(fib_entry, new_fib_index); } else if (was_import && !is_import) { diff --git a/src/vnet/fib/fib_table.c b/src/vnet/fib/fib_table.c index a928e77bbbc1..44f4e4485fd2 100644 --- a/src/vnet/fib/fib_table.c +++ b/src/vnet/fib/fib_table.c @@ -61,11 +61,53 @@ fib_table_lookup_i (fib_table_t *fib_table, return (FIB_NODE_INDEX_INVALID); } +/* + * fib_table_get() indexes the per-protocol FIB pool without validating the + * index. Callers that derive the index from an interface binding can be + * handed ~0, and an interface that has been deleted keeps a stale index until + * its binding is torn down. Validate here so that a bad index produces an + * empty lookup rather than a read through a wild pointer. + */ +static fib_table_t * +fib_table_get_or_null (u32 fib_index, fib_protocol_t proto) +{ + if (~0 == fib_index) + return (NULL); + + switch (proto) + { + case FIB_PROTOCOL_IP4: + if (fib_index >= vec_len (ip4_main.fibs) || + pool_is_free_index (ip4_main.fibs, fib_index)) + return (NULL); + break; + case FIB_PROTOCOL_IP6: + if (fib_index >= vec_len (ip6_main.fibs) || + pool_is_free_index (ip6_main.fibs, fib_index)) + return (NULL); + break; + case FIB_PROTOCOL_MPLS: + if (fib_index >= vec_len (mpls_main.fibs) || + pool_is_free_index (mpls_main.fibs, fib_index)) + return (NULL); + break; + } + + return (fib_table_get (fib_index, proto)); +} + fib_node_index_t fib_table_lookup (u32 fib_index, const fib_prefix_t *prefix) { - return (fib_table_lookup_i(fib_table_get(fib_index, prefix->fp_proto), prefix)); + fib_table_t *fib_table; + + fib_table = fib_table_get_or_null (fib_index, prefix->fp_proto); + + if (NULL == fib_table) + return (FIB_NODE_INDEX_INVALID); + + return (fib_table_lookup_i(fib_table, prefix)); } static inline fib_node_index_t @@ -94,9 +136,14 @@ fib_node_index_t fib_table_lookup_exact_match (u32 fib_index, const fib_prefix_t *prefix) { - return (fib_table_lookup_exact_match_i(fib_table_get(fib_index, - prefix->fp_proto), - prefix)); + fib_table_t *fib_table; + + fib_table = fib_table_get_or_null (fib_index, prefix->fp_proto); + + if (NULL == fib_table) + return (FIB_NODE_INDEX_INVALID); + + return (fib_table_lookup_exact_match_i (fib_table, prefix)); } static fib_node_index_t From d1d3cc6004005e2ed3459410dd516064faa19e07 Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 15 Sep 2026 23:42:16 +0800 Subject: [PATCH 49/85] linux-cp: drop nexthops on deleted interfaces lcp_router_route_path_parse() resolved a netlink nexthop to a sw_if_index through a hash lookup plus a pool index, which says nothing about whether the interface still exists. A route notification can name an interface that has already been removed, which happens routinely while the netlink state is replayed after a socket error: the link dump deletes LCP pairs before the route dump is read. The stale sw_if_index was then written into the FIB path and later turned into an out-of-range fib_index. Validate the interface and skip the nexthop when it is gone. Also remove lcp_routing_itf_db: its hash is never written, so the lookup always missed. It has been dead since it was introduced. Type: fix --- src/plugins/linux-cp/lcp_router.c | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/plugins/linux-cp/lcp_router.c b/src/plugins/linux-cp/lcp_router.c index 47b508845bd1..5a2236c712f4 100644 --- a/src/plugins/linux-cp/lcp_router.c +++ b/src/plugins/linux-cp/lcp_router.c @@ -85,26 +85,15 @@ static const mfib_prefix_t ip6_specials[] = { }, }; -/* VIF to PHY DB of managed interfaces */ -static uword *lcp_routing_itf_db; - static u32 lcp_router_intf_h2p (u32 host) { lcp_itf_pair_t *lip; index_t lipi; - uword *p; - - /* - * first check the linux side created interface (i.e. vlans, tunnels etc) - */ - p = hash_get (lcp_routing_itf_db, host); - - if (p) - return p[0]; /* - * then check the paired phys + * Find the PHY interface paired with the Linux VIF named in the netlink + * message. */ lipi = lcp_itf_pair_find_by_vif (host); @@ -1131,6 +1120,22 @@ lcp_router_route_path_parse (struct rtnl_nexthop *rnh, void *arg) sw_if_index = lcp_router_intf_h2p (rtnl_route_nh_get_ifindex (rnh)); + /* + * The pair lookup is a hash lookup plus a pool index; it does not tell us + * whether the interface still exists. A route notification can name an + * interface that has already been removed, which happens routinely while + * the netlink state is being replayed after a socket error: the link dump + * deletes LCP pairs before the route dump is read. Dropping the nexthop + * here keeps a stale sw_if_index out of the FIB, where it would later be + * turned into an out-of-range fib_index. + */ + if (~0 != sw_if_index && + NULL == vnet_get_sw_interface_or_null (vnet_get_main (), sw_if_index)) + { + LCP_ROUTER_DBG (" skipping nexthop on dead sw_if_index %u", sw_if_index); + sw_if_index = ~0; + } + if (~0 != sw_if_index) { fib_protocol_t fproto; From 7ee6487a3c2f422eb7c68c9b990ee1af8aa39520 Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Wed, 16 Sep 2026 01:11:16 +0800 Subject: [PATCH 50/85] build: make deb version vary per commit Both halves of the version pipeline drop the commit identifier when the repo carries no matching v* tag, so every build produces the identical version string "0.0.0-release": - the workflow synthesises "0.0.0-0-g", whose commit count and hash the version script then discards because git describe fails in a repo with no tags; - the script's git-describe fallback was only taken when git rev-parse failed, which it never does inside a git checkout. Identical version strings across builds make apt treat a rebuilt package as already installed and skip it, so a partial upgrade leaves mismatched libraries on disk. That is what broke the Sep 3 install: libvppinfra stayed at the July build while libvlib and the plugins moved to Sep 3, and the resulting clib_pmalloc_main_t layout mismatch segfaulted vlib_physmem_shared_map_create at startup. Carry the commit identifier through: the workflow now writes the commit timestamp instead of a literal 0 (this job clones with --depth 1, where git rev-list --count is always 1), and the script falls back to the identifier embedded in vstring whenever git describe yields nothing. Tagged repositories are unaffected; the script still exits 1 when there is neither a .version file nor a usable tag. --- .github/workflows/build-deb-trixie.yml | 9 ++++++++- src/scripts/version | 11 +++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-deb-trixie.yml b/.github/workflows/build-deb-trixie.yml index e71233399114..66feee4f3ab1 100644 --- a/.github/workflows/build-deb-trixie.yml +++ b/.github/workflows/build-deb-trixie.yml @@ -78,7 +78,14 @@ jobs: git config --global --add safe.directory "$PWD" git fetch --tags --force 2>/dev/null || true if ! git describe --tags --long --match 'v*' 2>/dev/null; then - echo "0.0.0-0-g$(git rev-parse --short=7 HEAD)" > src/scripts/.version + # No matching tag in this repo, so synthesise one. The build + # identifier must vary per commit, otherwise every build of this + # branch produces the same version string and apt skips packages + # it considers already installed. + # Use the commit timestamp rather than git rev-list --count: + # this job clones with --depth 1, where the count is always 1. + echo "0.0.0-$(git log -1 --format=%ct)-g$(git rev-parse --short=7 HEAD)" \ + > src/scripts/.version fi ccache -z export VPP_EXTRA_CMAKE_ARGS="\ diff --git a/src/scripts/version b/src/scripts/version index b8748af1588d..7bfe9daf7655 100755 --- a/src/scripts/version +++ b/src/scripts/version @@ -41,10 +41,17 @@ if [ -z "${POINT}" ]; then fi fi +# Prefer git for the commit description, but fall back to the commit count +# and hash already carried in vstring when git cannot supply them. A repo +# without any matching tag (e.g. a fork with no tags pushed) makes +# git describe fail; without this the commit is dropped and every build of +# such a repo gets the same version string, which makes package managers +# treat a newer build as already installed. git rev-parse 2> /dev/null if [ $? == 0 ]; then - CMT=$(git describe --dirty --long --match 'v*'| cut -s -d- -f3,4) -else + CMT=$(git describe --dirty --long --match 'v*' 2> /dev/null | cut -s -d- -f3,4) +fi +if [ -z "${CMT}" ]; then CMT=$(echo ${vstring} | cut -s -d- -f3,4) fi CMTR=$(echo $CMT | sed 's/-/_/') From bc83108750d2c4f64a0ff958c7b4d414fe905c65 Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Wed, 16 Sep 2026 01:13:38 +0800 Subject: [PATCH 51/85] build: keep synthesised deb version above 0.0.0-release The first form of the synthesised version, "0.0.0-~g", still sorts below the "0.0.0-release" that this branch has already shipped. Debian compares "-" by the LAST hyphen, so the two split into upstream "0.0.0" with revisions "release" and "~g", and "r" outranks "1". apt would therefore have refused the rebuilt packages and left libvppinfra on the old build - the exact failure this change set out to fix. Extend the existing "-release" revision instead, giving "0.0.0-release+~g", which dpkg orders strictly above "0.0.0-release" and monotonically across builds. --- .github/workflows/build-deb-trixie.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-deb-trixie.yml b/.github/workflows/build-deb-trixie.yml index 66feee4f3ab1..753cc19ac03d 100644 --- a/.github/workflows/build-deb-trixie.yml +++ b/.github/workflows/build-deb-trixie.yml @@ -82,9 +82,17 @@ jobs: # identifier must vary per commit, otherwise every build of this # branch produces the same version string and apt skips packages # it considers already installed. + # + # The "0.0.0-release" prefix is deliberate: it is the version of + # packages already installed from this branch, and Debian + # compares "-" by its last hyphen, so a plain + # "0.0.0-~g" would sort BELOW "0.0.0-release" (letters + # outrank digits) and apt would refuse it. Extending the existing + # "-release" revision keeps every new build strictly newer. + # # Use the commit timestamp rather than git rev-list --count: # this job clones with --depth 1, where the count is always 1. - echo "0.0.0-$(git log -1 --format=%ct)-g$(git rev-parse --short=7 HEAD)" \ + echo "0.0.0-release+$(git log -1 --format=%ct)-g$(git rev-parse --short=7 HEAD)" \ > src/scripts/.version fi ccache -z From 19caf6f9f590f7acc94667ffe84a7692716c6a68 Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 22 Sep 2026 01:46:16 +0800 Subject: [PATCH 52/85] flowprobe: record l2 flows on vlan sub-interfaces (#7) The l2 datapath could only be enabled on a physical port: it is installed on the device-input and interface-output arcs, which are evaluated against the physical interface index. Enabling it on a VLAN sub-interface therefore installed the feature on arcs that no packet of that sub-interface ever reaches, so the command was accepted and then silently recorded nothing. Install the l2 datapath on the ip4/ip6 input and output arcs of a sub-interface instead, selected with vnet_sw_interface_is_sub() when the feature is enabled. On the receive side of such a packet ethernet-input has already consumed the L2 header, so take the Ethernet header from l2_hdr_offset when it is valid, and derive the recorded ethertype and the L3 offset from one bounded walk over the (possibly stacked) VLAN tags. Enabling the l2 datapath also switches template_reports[] and template_size[] from a record-flags index to the variant index. The raw flags cannot key a per-variant template: with `record l2 l3 l4` the l2, ip4 and ip6 variants all write slot 7, so enabling two of them on different interfaces made each export the other one's template id and record size. The l2 datapath cannot coexist with the ip datapaths without this. Records for tagged frames are now selected by the innermost ethertype. On a frame tagged 0x8100 the recorded ethernetType was already the inner one, but the variant was chosen from the outer tag type, so an IPv4 frame was reported into the L2-only template; it now goes into the IPv4 template. Frames tagged 0x88a8, 0x9100 and 0x9200 additionally report the inner ethertype as ethernetType where the outer tag type was reported before. The flow key always carries the innermost ethertype now instead of only for 0x8100 frames. On a sub-interface only IPv4 and IPv6 frames are recorded: ARP, LLDP and frames that stay inside a bridge domain do not traverse the arcs the feature is installed on. Type: feature --- src/plugins/flowprobe/FEATURE.yaml | 1 + src/plugins/flowprobe/flowprobe.c | 112 ++++++++++-- src/plugins/flowprobe/flowprobe.h | 8 +- .../flowprobe/flowprobe_plugin_doc.rst | 10 ++ src/plugins/flowprobe/node.c | 90 ++++++---- test/test_flowprobe.py | 169 +++++++++++++++++- 6 files changed, 340 insertions(+), 50 deletions(-) diff --git a/src/plugins/flowprobe/FEATURE.yaml b/src/plugins/flowprobe/FEATURE.yaml index 9c80b12dc9f4..ee5c12212dd2 100644 --- a/src/plugins/flowprobe/FEATURE.yaml +++ b/src/plugins/flowprobe/FEATURE.yaml @@ -4,6 +4,7 @@ maintainer: Ole Troan features: - L2 input and output feature path - IPv4 / IPv6 input and output feature path + - L2 record on VLAN sub-interfaces for IPv4 / IPv6 traffic - Recording of L2, L3, and L4 information description: "IPFIX flow probe. Works in the L2 or IP feature path both input and output." missing: diff --git a/src/plugins/flowprobe/flowprobe.c b/src/plugins/flowprobe/flowprobe.c index 38da54022c43..154a54f1bb88 100644 --- a/src/plugins/flowprobe/flowprobe.c +++ b/src/plugins/flowprobe/flowprobe.c @@ -77,6 +77,46 @@ VNET_FEATURE_INIT (flowprobe_output_l2, static) = { .runs_before = VNET_FEATURES ("interface-output-arc-end"), }; +/* + * On a VLAN sub-interface the L2 variant cannot use the device-input and + * interface-output arcs: both are evaluated against the physical interface + * index (device-input runs before ethernet-input has matched the tag, and + * interface-output uses the parent port's runtime index). Register the same + * node on the L3 input/output arcs instead; there VLIB_RX/VLIB_TX already + * hold the sub-interface index. flowprobe_feature_enable_disable() selects + * between the two sets based on vnet_sw_interface_is_sub(). + */ +VNET_FEATURE_INIT (flowprobe_input_l2_ip4_unicast, static) = { + .arc_name = "ip4-unicast", + .node_name = "flowprobe-input-l2", + .runs_before = VNET_FEATURES ("ip4-lookup", "ip4-inacl"), +}; +VNET_FEATURE_INIT (flowprobe_input_l2_ip4_multicast, static) = { + .arc_name = "ip4-multicast", + .node_name = "flowprobe-input-l2", + .runs_before = VNET_FEATURES ("ip4-mfib-forward-lookup"), +}; +VNET_FEATURE_INIT (flowprobe_input_l2_ip6_unicast, static) = { + .arc_name = "ip6-unicast", + .node_name = "flowprobe-input-l2", + .runs_before = VNET_FEATURES ("ip6-lookup", "ip6-inacl"), +}; +VNET_FEATURE_INIT (flowprobe_input_l2_ip6_multicast, static) = { + .arc_name = "ip6-multicast", + .node_name = "flowprobe-input-l2", + .runs_before = VNET_FEATURES ("ip6-mfib-forward-lookup"), +}; +VNET_FEATURE_INIT (flowprobe_output_l2_ip4, static) = { + .arc_name = "ip4-output", + .node_name = "flowprobe-output-l2", + .runs_before = VNET_FEATURES ("interface-output"), +}; +VNET_FEATURE_INIT (flowprobe_output_l2_ip6, static) = { + .arc_name = "ip6-output", + .node_name = "flowprobe-output-l2", + .runs_before = VNET_FEATURES ("interface-output"), +}; + #define FINISH \ vec_add1 (s, 0); \ vlib_cli_output (handle, (char *) s); \ @@ -307,7 +347,7 @@ flowprobe_template_rewrite_inline (ipfix_exporter_t *exp, flow_report_t *fr, /* Field count in this template */ t->id_count = ipfix_id_count (fr->template_id, f - first_field); - fm->template_size[flags] = (u8 *) f - (u8 *) s; + fm->template_size[which] = (u8 *) f - (u8 *) s; /* set length in octets */ s->set_id_length = @@ -430,7 +470,11 @@ flowprobe_template_add_del (u32 domain_id, u16 src_port, typedef struct { - flowprobe_record_t report_flags; + /* Variant that owns the template slot. The raw record flags cannot be used + * as the slot key: IP4, IP6 and L2-plain all report the same flags value, so + * indexing the per-variant template tables by flags makes them overwrite + * each other. */ + flowprobe_variant_t variant; vnet_flow_data_callback_t *flow_data_callback; vnet_flow_rewrite_callback_t *rewrite_callback; } flowprobe_template_spec_t; @@ -445,19 +489,19 @@ flowprobe_get_template_specs (flowprobe_variant_t which, flowprobe_record_t flag { if (flags & FLOW_RECORD_L2) specs[n_specs++] = (flowprobe_template_spec_t) { - .report_flags = flags, + .variant = FLOW_VARIANT_L2, .flow_data_callback = flowprobe_data_callback_l2, .rewrite_callback = flowprobe_template_rewrite_l2, }; if (flags & FLOW_RECORD_L3 || flags & FLOW_RECORD_L4) { specs[n_specs++] = (flowprobe_template_spec_t) { - .report_flags = flags | FLOW_RECORD_L2_IP4, + .variant = FLOW_VARIANT_L2_IP4, .flow_data_callback = flowprobe_data_callback_l2, .rewrite_callback = flowprobe_template_rewrite_l2_ip4, }; specs[n_specs++] = (flowprobe_template_spec_t) { - .report_flags = flags | FLOW_RECORD_L2_IP6, + .variant = FLOW_VARIANT_L2_IP6, .flow_data_callback = flowprobe_data_callback_l2, .rewrite_callback = flowprobe_template_rewrite_l2_ip6, }; @@ -465,13 +509,13 @@ flowprobe_get_template_specs (flowprobe_variant_t which, flowprobe_record_t flag } else if (which == FLOW_VARIANT_IP4) specs[n_specs++] = (flowprobe_template_spec_t) { - .report_flags = flags, + .variant = FLOW_VARIANT_IP4, .flow_data_callback = flowprobe_data_callback_ip4, .rewrite_callback = flowprobe_template_rewrite_ip4, }; else if (which == FLOW_VARIANT_IP6) specs[n_specs++] = (flowprobe_template_spec_t) { - .report_flags = flags, + .variant = FLOW_VARIANT_IP6, .flow_data_callback = flowprobe_data_callback_ip6, .rewrite_callback = flowprobe_template_rewrite_ip6, }; @@ -497,7 +541,7 @@ flowprobe_templates_add_del (flowprobe_main_t *fm, flowprobe_variant_t which, for (i = 0; i < n_specs; i++) { - old_template_ids[i] = fm->template_reports[specs[i].report_flags]; + old_template_ids[i] = fm->template_reports[specs[i].variant]; template_id = old_template_ids[i]; rv = flowprobe_template_add_del (1, UDP_DST_PORT_ipfix, flags, specs[i].flow_data_callback, specs[i].rewrite_callback, is_add, &template_id); @@ -505,7 +549,7 @@ flowprobe_templates_add_del (flowprobe_main_t *fm, flowprobe_variant_t which, goto rollback; changed[i] = rv == 0; - fm->template_reports[specs[i].report_flags] = is_add ? template_id : 0; + fm->template_reports[specs[i].variant] = is_add ? template_id : 0; } return 0; @@ -518,7 +562,7 @@ flowprobe_templates_add_del (flowprobe_main_t *fm, flowprobe_variant_t which, i--; if (!changed[i]) { - fm->template_reports[specs[i].report_flags] = old_template_ids[i]; + fm->template_reports[specs[i].variant] = old_template_ids[i]; continue; } @@ -531,7 +575,7 @@ flowprobe_templates_add_del (flowprobe_main_t *fm, flowprobe_variant_t which, *state_restored = false; continue; } - fm->template_reports[specs[i].report_flags] = is_add ? old_template_ids[i] : template_id; + fm->template_reports[specs[i].variant] = is_add ? old_template_ids[i] : template_id; } return rv; @@ -687,6 +731,12 @@ flowprobe_flush_callback (flowprobe_variant_t which) static void flowprobe_feature_enable_disable (u32 sw_if_index, u8 which, u8 direction, int enable) { + flowprobe_main_t *fm = &flowprobe_main; + + /* Only meaningful on enable, where the interface type picks the arc set. On + disable both sets are cleared anyway, see below. */ + bool is_sub = enable && vnet_sw_interface_is_sub (fm->vnet_main, sw_if_index); + if (direction == FLOW_DIRECTION_RX || direction == FLOW_DIRECTION_BOTH) { if (which == FLOW_VARIANT_IP4) @@ -704,8 +754,29 @@ flowprobe_feature_enable_disable (u32 sw_if_index, u8 which, u8 direction, int e 0, 0); } else if (which == FLOW_VARIANT_L2) - vnet_feature_enable_disable ("device-input", "flowprobe-input-l2", sw_if_index, enable, 0, - 0); + { + /* + * A VLAN sub-interface is reached through the L3 feature arcs, a + * physical port through device-input. On disable both sets are + * cleared: the interface may have changed type, or its index may have + * been reused, since the feature was enabled, and clearing a feature + * that was never set on an arc is a no-op. + */ + if (!enable || is_sub) + { + vnet_feature_enable_disable ("ip4-unicast", "flowprobe-input-l2", sw_if_index, enable, + 0, 0); + vnet_feature_enable_disable ("ip4-multicast", "flowprobe-input-l2", sw_if_index, + enable, 0, 0); + vnet_feature_enable_disable ("ip6-unicast", "flowprobe-input-l2", sw_if_index, enable, + 0, 0); + vnet_feature_enable_disable ("ip6-multicast", "flowprobe-input-l2", sw_if_index, + enable, 0, 0); + } + if (!enable || !is_sub) + vnet_feature_enable_disable ("device-input", "flowprobe-input-l2", sw_if_index, enable, + 0, 0); + } } if (direction == FLOW_DIRECTION_TX || direction == FLOW_DIRECTION_BOTH) @@ -717,8 +788,19 @@ flowprobe_feature_enable_disable (u32 sw_if_index, u8 which, u8 direction, int e vnet_feature_enable_disable ("ip6-output", "flowprobe-output-ip6", sw_if_index, enable, 0, 0); else if (which == FLOW_VARIANT_L2) - vnet_feature_enable_disable ("interface-output", "flowprobe-output-l2", sw_if_index, enable, - 0, 0); + { + /* Same split as RX above, see flowprobe-input-l2. */ + if (!enable || is_sub) + { + vnet_feature_enable_disable ("ip4-output", "flowprobe-output-l2", sw_if_index, enable, + 0, 0); + vnet_feature_enable_disable ("ip6-output", "flowprobe-output-l2", sw_if_index, enable, + 0, 0); + } + if (!enable || !is_sub) + vnet_feature_enable_disable ("interface-output", "flowprobe-output-l2", sw_if_index, + enable, 0, 0); + } } } diff --git a/src/plugins/flowprobe/flowprobe.h b/src/plugins/flowprobe/flowprobe.h index 8fbca0d9ac39..3f13335512ea 100644 --- a/src/plugins/flowprobe/flowprobe.h +++ b/src/plugins/flowprobe/flowprobe.h @@ -29,7 +29,6 @@ typedef enum FLOW_RECORD_L4 = 1 << 2, FLOW_RECORD_L2_IP4 = 1 << 3, FLOW_RECORD_L2_IP6 = 1 << 4, - FLOW_N_RECORDS = 1 << 5, } flowprobe_record_t; typedef enum __attribute__ ((__packed__)) @@ -117,8 +116,11 @@ typedef struct u16 msg_id_base; flowprobe_protocol_context_t context[FLOW_N_VARIANTS]; - u16 template_reports[FLOW_N_RECORDS]; - u16 template_size[FLOW_N_RECORDS]; + /* Indexed by flowprobe_variant_t. Each variant owns an independent + * template slot so that IP4, IP6, L2, L2_IP4 and L2_IP6 cannot overwrite + * one another when they share the same record flags. */ + u16 template_reports[FLOW_N_VARIANTS]; + u16 template_size[FLOW_N_VARIANTS]; /** Time reference pair */ u64 nanosecond_time_0; diff --git a/src/plugins/flowprobe/flowprobe_plugin_doc.rst b/src/plugins/flowprobe/flowprobe_plugin_doc.rst index 4add41f5611f..eeb6ad7e8a6b 100644 --- a/src/plugins/flowprobe/flowprobe_plugin_doc.rst +++ b/src/plugins/flowprobe/flowprobe_plugin_doc.rst @@ -16,3 +16,13 @@ Sample configuration flowprobe params record l3 active 20 passive 120 flowprobe feature add-del GigabitEthernet2/3/0 l2 + +The ``l2`` datapath can also be enabled on a VLAN sub-interface, where it is +installed on the IPv4 and IPv6 feature arcs of that sub-interface. When the +record includes ``l2``, it records the MAC addresses and the inner ethertype +of IPv4 and IPv6 frames of the enabled directions. Non-IP frames are not +recorded on a sub-interface. + +:: + + flowprobe feature add-del GigabitEthernet2/3/0.10 l2 both diff --git a/src/plugins/flowprobe/node.c b/src/plugins/flowprobe/node.c index b2bb56311375..a4159366881f 100644 --- a/src/plugins/flowprobe/node.c +++ b/src/plugins/flowprobe/node.c @@ -132,6 +132,53 @@ typedef enum [FLOWPROBE_NEXT_IP4_LOOKUP] = "ip4-lookup", \ } +/* + * The L2 variant runs on two different kinds of arc. On device-input / + * interface-output (physical ports) current_data points at the Ethernet + * header. On the L3 feature arcs ethernet-input has already consumed the L2 + * header on RX, so the Ethernet header must be taken from l2_hdr_offset; on TX + * the rewritten header is again at current_data. + */ +static_always_inline ethernet_header_t * +flowprobe_eth_header (vlib_buffer_t *b, flowprobe_direction_t direction) +{ + if (direction == FLOW_DIRECTION_RX && (b->flags & VNET_BUFFER_F_L2_HDR_OFFSET_VALID)) + return ethernet_buffer_get_header (b); + return vlib_buffer_get_current (b); +} + +/* + * Walk the (possibly stacked) VLAN tags and return the innermost ethertype, + * counting the tags in *n_tags. The walk stops at the end of the frame, so a + * truncated tag header is never read. + */ +static_always_inline u16 +flowprobe_vlan_walk (vlib_buffer_t *b, flowprobe_direction_t direction, u16 *n_tags) +{ + ethernet_header_t *eth = flowprobe_eth_header (b, direction); + ethernet_vlan_header_tv_t *ethv = (ethernet_vlan_header_tv_t *) ð->type; + u8 *end = b->data + b->current_data + b->current_length; + u16 ethertype = clib_net_to_host_u16 (ethv->type); + + *n_tags = 0; + while (ethernet_frame_is_tagged (ethertype) && (u8 *) (ethv + 1) + sizeof (ethv->type) <= end) + { + ethv++; + (*n_tags)++; + ethertype = clib_net_to_host_u16 (ethv->type); + } + return ethertype; +} + +/* Innermost ethertype, skipping any (stacked) VLAN tags. */ +static_always_inline u16 +flowprobe_inner_ethertype (vlib_buffer_t *b, flowprobe_direction_t direction) +{ + u16 n_tags; + + return flowprobe_vlan_walk (b, direction, &n_tags); +} + static inline flowprobe_variant_t flowprobe_get_variant (flowprobe_variant_t which, flowprobe_record_t flags, u16 ethertype) @@ -377,11 +424,11 @@ add_to_flow_record_state (vlib_main_t *vm, vlib_node_runtime_t *node, flowprobe_record_t flags = fm->context[which].flags; bool collect_ip4 = false, collect_ip6 = false; ASSERT (b); - ethernet_header_t *eth = (direction == FLOW_DIRECTION_TX) ? - vlib_buffer_get_current (b) : - ethernet_buffer_get_header (b); - u16 ethertype = clib_net_to_host_u16 (eth->type); - i16 l3_hdr_offset = (u8 *) eth - b->data + sizeof (ethernet_header_t); + ethernet_header_t *eth = flowprobe_eth_header (b, direction); + u16 n_tags = 0; + u16 ethertype = flowprobe_vlan_walk (b, direction, &n_tags); + i16 l3_hdr_offset = + (u8 *) eth - b->data + sizeof (ethernet_header_t) + n_tags * sizeof (ethernet_vlan_header_tv_t); flowprobe_key_t k = {}; ip4_header_t *ip4 = 0; ip6_header_t *ip6 = 0; @@ -400,25 +447,12 @@ add_to_flow_record_state (vlib_main_t *vm, vlib_node_runtime_t *node, k.which = which; k.direction = direction; + k.ethertype = ethertype; if (flags & FLOW_RECORD_L2) { clib_memcpy_fast (k.src_mac, eth->src_address, 6); clib_memcpy_fast (k.dst_mac, eth->dst_address, 6); - k.ethertype = ethertype; - } - if (ethertype == ETHERNET_TYPE_VLAN) - { - /*VLAN TAG*/ - ethernet_vlan_header_tv_t *ethv = - (ethernet_vlan_header_tv_t *) (&(eth->type)); - /*Q in Q possibility */ - while (clib_net_to_host_u16 (ethv->type) == ETHERNET_TYPE_VLAN) - { - ethv++; - l3_hdr_offset += sizeof (ethernet_vlan_header_tv_t); - } - k.ethertype = ethertype = clib_net_to_host_u16 ((ethv)->type); } if (collect_ip6 && ethertype == ETHERNET_TYPE_IP6) { @@ -548,7 +582,6 @@ flowprobe_export_send (vlib_main_t * vm, vlib_buffer_t * b0, ipfix_message_header_t *h; ip4_header_t *ip; udp_header_t *udp; - flowprobe_record_t flags = fm->context[which].flags; u32 my_cpu_number = vm->thread_index; /* Fill in header */ @@ -599,10 +632,8 @@ flowprobe_export_send (vlib_main_t * vm, vlib_buffer_t * b0, h->sequence_number = stream->sequence_number++; h->sequence_number = clib_host_to_net_u32 (h->sequence_number); - s->set_id_length = ipfix_set_id_length (fm->template_reports[flags], - b0->current_length - - (sizeof (*ip) + sizeof (*udp) + - sizeof (*h))); + s->set_id_length = ipfix_set_id_length ( + fm->template_reports[which], b0->current_length - (sizeof (*ip) + sizeof (*udp) + sizeof (*h))); h->version_length = version_length (b0->current_length - (sizeof (*ip) + sizeof (*udp))); @@ -738,7 +769,7 @@ flowprobe_export_entry (vlib_main_t * vm, flowprobe_entry_t * e) fm->context[which].next_record_offset_per_worker[my_cpu_number] = offset; /* Time to flush the buffer? */ - if (offset + fm->template_size[flags] > exp->path_mtu) + if (offset + fm->template_size[which] > exp->path_mtu) flowprobe_export_send (vm, b0, which); } @@ -801,8 +832,7 @@ flowprobe_node_fn (vlib_main_t *vm, vlib_node_runtime_t *node, vnet_feature_next (&next1, b1); len0 = vlib_buffer_length_in_chain (vm, b0); - ethernet_header_t *eh0 = vlib_buffer_get_current (b0); - u16 ethertype0 = clib_net_to_host_u16 (eh0->type); + u16 ethertype0 = flowprobe_inner_ethertype (b0, direction); if (PREDICT_TRUE ((b0->flags & VNET_BUFFER_F_FLOW_REPORT) == 0)) add_to_flow_record_state ( @@ -812,8 +842,7 @@ flowprobe_node_fn (vlib_main_t *vm, vlib_node_runtime_t *node, direction, 0); len1 = vlib_buffer_length_in_chain (vm, b1); - ethernet_header_t *eh1 = vlib_buffer_get_current (b1); - u16 ethertype1 = clib_net_to_host_u16 (eh1->type); + u16 ethertype1 = flowprobe_inner_ethertype (b1, direction); if (PREDICT_TRUE ((b1->flags & VNET_BUFFER_F_FLOW_REPORT) == 0)) add_to_flow_record_state ( @@ -848,8 +877,7 @@ flowprobe_node_fn (vlib_main_t *vm, vlib_node_runtime_t *node, vnet_feature_next (&next0, b0); len0 = vlib_buffer_length_in_chain (vm, b0); - ethernet_header_t *eh0 = vlib_buffer_get_current (b0); - u16 ethertype0 = clib_net_to_host_u16 (eh0->type); + u16 ethertype0 = flowprobe_inner_ethertype (b0, direction); if (PREDICT_TRUE ((b0->flags & VNET_BUFFER_F_FLOW_REPORT) == 0)) { diff --git a/test/test_flowprobe.py b/test/test_flowprobe.py index 6d9d5c49a3ee..c9219ca2b770 100644 --- a/test/test_flowprobe.py +++ b/test/test_flowprobe.py @@ -68,7 +68,8 @@ def __init__( ): self._test = test self._intf = intf - self._intf_obj = getattr(self._test, intf) + # intf is an attribute name of the test case, or an interface object + self._intf_obj = getattr(self._test, intf) if isinstance(intf, str) else intf self._active = active if passive == 0 or passive < active: self._passive = active + 1 @@ -1316,6 +1317,172 @@ class DatapathRx(MethodHolder, DatapathTestsHolder): direction = "rx" +@unittest.skipIf( + "flowprobe" in config.excluded_plugins, "Exclude Flowprobe plugin tests" +) +class DatapathSubInterface(MethodHolder): + """L2 datapath enabled on a VLAN sub-interface (no timers)""" + + def test_L2onSubInterface(self): + """L2, L3 and L4 data on the L2 datapath of a sub-interface""" + self.pg_enable_capture(self.pg_interfaces) + self.pkts = [] + + # a dot1ad sub-interface on a physical port + subif = VppDot1ADSubint(self, self.pg7, 0, 300, 400) + subif.admin_up() + subif.config_ip4() + subif.config_ip6() + + # enable the feature on the sub-interface, not on the physical port + ipfix = VppCFLOW(test=self, intf=subif, layer="l2 l3 l4", direction="rx") + ipfix.add_vpp_config() + + ipfix_decoder = IPFIXDecoder() + tmpl_l2_field_count = TMPL_COMMON_FIELD_COUNT + TMPL_L2_FIELD_COUNT + tmpl_ip_field_count = ( + TMPL_COMMON_FIELD_COUNT + + TMPL_L2_FIELD_COUNT + + TMPL_L3_FIELD_COUNT + + TMPL_L4_FIELD_COUNT + ) + # template 0 is L2 only, 1 is IPv4 and 2 is IPv6 + templates = ipfix.verify_templates( + ipfix_decoder, + count=3, + field_count_in=(tmpl_l2_field_count, tmpl_ip_field_count), + ) + + for ip_ver in ("v4", "v6"): + pkt = Ether(src=subif.remote_mac, dst=self.pg7.local_mac) + if ip_ver == "v4": + src_ip, dst_ip = subif.remote_ip4, "9.0.0.1" + src_ip_id = IPFIX_SRC_IP4_ADDR_ID + dst_ip_id = IPFIX_DST_IP4_ADDR_ID + ethertype = 8 + family = socket.AF_INET + pkt /= IP(src=src_ip, dst=dst_ip) + else: + src_ip, dst_ip = subif.remote_ip6, "2001:db8:9::1" + src_ip_id = 27 + dst_ip_id = 28 + ethertype = 56710 + family = socket.AF_INET6 + pkt /= IPv6(src=src_ip, dst=dst_ip) + pkt /= UDP(sport=1234, dport=4321) + pkt /= Raw(b"\xa5" * 32) + + self.pkts = [subif.add_dot1ad_layer(pkt, 300, 400)] + self.pg7.add_stream(self.pkts) + self.pg_start() + + # make sure the flow we expect actually showed up + self.vapi.ipfix_flush() + cflow = self.wait_for_cflow_packet( + self.collector, templates[1 if ip_ver == "v4" else 2], 15 + ) + data = ipfix_decoder.decode_data_set(cflow.getlayer(Set)) + self.assertEqual(len(data), 1) + record = data[0] + self.assertEqual(len(record), tmpl_ip_field_count) + # the flow is keyed on the sub-interface, not on its parent + self.assertEqual(int(binascii.hexlify(record[10]), 16), subif.sw_if_index) + self.assertEqual(int(binascii.hexlify(record[256]), 16), ethertype) + self.assertEqual(int(binascii.hexlify(record[4]), 16), 17) + self.assertEqual(int(binascii.hexlify(record[7]), 16), 1234) + self.assertEqual(int(binascii.hexlify(record[11]), 16), 4321) + self.assertEqual( + int(binascii.hexlify(record[IPFIX_FLOW_DIRECTION_ID]), 16), 0 + ) + self.assertEqual( + int(binascii.hexlify(record[src_ip_id]), 16), + int(binascii.hexlify(socket.inet_pton(family, src_ip)), 16), + ) + self.assertEqual( + int(binascii.hexlify(record[dst_ip_id]), 16), + int(binascii.hexlify(socket.inet_pton(family, dst_ip)), 16), + ) + + # cleanup, the feature must be disabled before the sub-interface goes away + ipfix.remove_vpp_config() + subif.remove_vpp_config() + + def test_L2onSubInterfaceTx(self): + """L2, L3 and L4 data on the egress side of a sub-interface""" + self.pg_enable_capture(self.pg_interfaces) + self.pkts = [] + + # a dot1ad sub-interface on a physical port; different tags than + # test_L2onSubInterface, so that one leaving its sub-interface behind + # cannot fail this test + subif = VppDot1ADSubint(self, self.pg7, 1, 301, 401) + subif.admin_up() + subif.config_ip4() + subif.configure_ipv4_neighbors() + + # enable the feature on the sub-interface, not on the physical port + ipfix = VppCFLOW(test=self, intf=subif, layer="l2 l3 l4", direction="tx") + ipfix.add_vpp_config() + + ipfix_decoder = IPFIXDecoder() + tmpl_l2_field_count = TMPL_COMMON_FIELD_COUNT + TMPL_L2_FIELD_COUNT + tmpl_ip_field_count = ( + TMPL_COMMON_FIELD_COUNT + + TMPL_L2_FIELD_COUNT + + TMPL_L3_FIELD_COUNT + + TMPL_L4_FIELD_COUNT + ) + templates = ipfix.verify_templates( + ipfix_decoder, + count=3, + field_count_in=(tmpl_l2_field_count, tmpl_ip_field_count), + ) + + # route the traffic out of the sub-interface + route = VppIpRoute( + self, + "9.0.0.0", + 24, + [VppRoutePath(subif.remote_ip4, subif.sw_if_index)], + ) + route.add_vpp_config() + + self.pkts = [ + Ether(src=self.pg8.remote_mac, dst=self.pg8.local_mac) + / IP(src=self.pg8.remote_ip4, dst="9.0.0.1") + / UDP(sport=1234, dport=4321) + / Raw(b"\xa5" * 32) + ] + self.pg8.add_stream(self.pkts) + self.pg_start() + + # make sure the flow we expect actually showed up + self.vapi.ipfix_flush() + cflow = self.wait_for_cflow_packet(self.collector, templates[1], 15) + data = ipfix_decoder.decode_data_set(cflow.getlayer(Set)) + self.assertEqual(len(data), 1) + record = data[0] + self.assertEqual(len(record), tmpl_ip_field_count) + # the flow belongs to the sub-interface the frames left through + self.assertEqual(int(binascii.hexlify(record[14]), 16), subif.sw_if_index) + self.assertEqual(int(binascii.hexlify(record[10]), 16), self.pg8.sw_if_index) + # the ethertype of the tagged frame is the inner one + self.assertEqual(int(binascii.hexlify(record[256]), 16), 8) + self.assertEqual(int(binascii.hexlify(record[4]), 16), 17) + self.assertEqual(int(binascii.hexlify(record[7]), 16), 1234) + self.assertEqual(int(binascii.hexlify(record[11]), 16), 4321) + self.assertEqual(int(binascii.hexlify(record[IPFIX_FLOW_DIRECTION_ID]), 16), 1) + self.assertEqual( + int(binascii.hexlify(record[IPFIX_DST_IP4_ADDR_ID]), 16), + int(binascii.hexlify(socket.inet_pton(socket.AF_INET, "9.0.0.1")), 16), + ) + + # cleanup, the feature must be disabled before the sub-interface goes away + ipfix.remove_vpp_config() + route.remove_vpp_config() + subif.remove_vpp_config() + + @unittest.skipUnless(config.extended, "part of extended tests") @unittest.skipIf( "flowprobe" in config.excluded_plugins, "Exclude Flowprobe plugin tests" From c1f4126d2029caaeb033dca641df682e49b3dada Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 22 Sep 2026 02:10:20 +0800 Subject: [PATCH 53/85] rdma: initialize the create-if args before freeing them faa9de939 added vec_free() of args.ifname and args.name to the failure branch of rdma_create_command_fn(), but unformat_rdma_create_if_args() returns 0 before its clib_memset() when the line is empty, so the two vec_free() calls ran on an uninitialized rdma_create_if_args_t. Freeing a stack garbage pointer corrupts the heap. Initialize the struct the way api.c does. Type: fix --- src/plugins/rdma/cli.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/rdma/cli.c b/src/plugins/rdma/cli.c index 99959f758f69..4190eb699465 100644 --- a/src/plugins/rdma/cli.c +++ b/src/plugins/rdma/cli.c @@ -18,7 +18,7 @@ static clib_error_t * rdma_create_command_fn (vlib_main_t * vm, unformat_input_t * input, vlib_cli_command_t * cmd) { - rdma_create_if_args_t args; + rdma_create_if_args_t args = { }; if (!unformat_user (input, unformat_rdma_create_if_args, &args)) { From be3bcc3320d4ea84bf5d446576c293ae0ce7a1c8 Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 22 Sep 2026 04:58:50 +0800 Subject: [PATCH 54/85] dpdk: clear the vnet rx-queue file index on interrupt teardown (#9) dpdk_remove_interrupt_files() deletes the clib file registered for each rx queue and resets the dpdk copy of its index, but the vnet rx queue holds the same index for rx-placement updates. A placement change after the interface was brought down resolved the freed clib file, handing NULL to vlib_file_update(). Clear the vnet copy before the file's pool slot is released, let the setter accept the ~0 "no file" value, and have the placement path drop an index that no longer resolves to a live file. Type: fix --- src/plugins/dpdk/device/common.c | 5 +++++ src/vnet/interface/rx_queue.c | 23 +++++++++++++++++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/plugins/dpdk/device/common.c b/src/plugins/dpdk/device/common.c index 00f456fe9b36..b7003a89905a 100644 --- a/src/plugins/dpdk/device/common.c +++ b/src/plugins/dpdk/device/common.c @@ -371,6 +371,7 @@ static void dpdk_remove_interrupt_files (dpdk_device_t *xd) { clib_file_main_t *fm = &file_main; + vnet_main_t *vnm = vnet_get_main (); for (int q = 0; q < vec_len (xd->rx_queues); q++) { @@ -386,6 +387,10 @@ dpdk_remove_interrupt_files (dpdk_device_t *xd) fm->file_update (f, UNIX_FILE_UPDATE_ADD); rxq->clib_file_registered = 1; } + /* The vnet rx queue keeps its own copy of the index for rx-placement + updates. Clear it before the file's pool slot is released, or the + next placement change on this queue resolves a freed file. */ + vnet_hw_if_set_rx_queue_file_index (vnm, rxq->queue_index, ~0); clib_file_del_by_index (fm, rxq->clib_file_index); rxq->clib_file_index = ~0; rxq->clib_file_registered = 0; diff --git a/src/vnet/interface/rx_queue.c b/src/vnet/interface/rx_queue.c index c5ca41bb59ac..0542da4b7108 100644 --- a/src/vnet/interface/rx_queue.c +++ b/src/vnet/interface/rx_queue.c @@ -150,7 +150,10 @@ vnet_hw_if_set_rx_queue_file_index (vnet_main_t *vnm, u32 queue_index, vnet_hw_interface_t *hi = vnet_get_hw_interface (vnm, rxq->hw_if_index); rxq->file_index = file_index; - clib_file_set_polling_thread (&file_main, file_index, rxq->thread_index); + /* ~0 means the queue has no clib file; any other value must name a live + one, because the poller is programmed for it. */ + if (file_index != ~0) + clib_file_set_polling_thread (&file_main, file_index, rxq->thread_index); log_debug ("set_file_index: interface %v queue-id %u file-index %u", hi->name, rxq->queue_id, file_index); } @@ -223,7 +226,23 @@ vnet_hw_if_set_rx_queue_thread_index (vnet_main_t *vnm, u32 queue_index, rxq->thread_index = thread_index; if (rxq->file_index != ~0) - clib_file_set_polling_thread (&file_main, rxq->file_index, thread_index); + { + clib_file_t *f = clib_file_get (&file_main, rxq->file_index); + + if (f) + clib_file_set_polling_thread (&file_main, rxq->file_index, + thread_index); + else + { + /* The file was removed without clearing the queue's copy, which + drivers do when they close their interrupt files. Drop the stale + index rather than resolving it again on the next placement. */ + log_debug ("set_rx_queue_thread_index: interface %v queue-id %u " + "file-index %u is gone", + hi->name, rxq->queue_id, rxq->file_index); + rxq->file_index = ~0; + } + } log_debug ("set_rx_queue_thread_index: interface %v queue-id %u " "thread-index set to %u", From 6d8c17601e149739975d3311f8235b9510575838 Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 22 Sep 2026 05:18:23 +0800 Subject: [PATCH 55/85] session: complete the rollback of a failed fifo migration (#10) * session: handle datagram fifo migration failures in callers session_dgram_connect_notify() returns an error when the fifos cannot be attached, without assigning *new_session. Neither caller checked: srtp then built a session handle from an uninitialised stack pointer, and udp_input cleared queue_event as if the session had been migrated, so the packet was enqueued without ever notifying the thread that owns the session. Keep the old session on failure in both callers. udp_input also undoes the transport clone, clears UDP_CONN_F_MIGRATED, delivers the datagram to the fifo of the session that stays in use and sends an RX event to the thread that owns it, which is what a local enqueue would have done. Type: fix * session: restore the lookup entry of a rolled back migration session_dgram_connect_notify() repoints the transport lookup entry at the cloned session before attaching the fifos. When an attach failed, the rollback deleted the entry instead of restoring it, so every later packet of the 5-tuple failed to find a session, although the session that was in use before the attempt was still alive. Point the entry back at the session that stays in use. Type: fix --- src/plugins/srtp/srtp.c | 9 +++++++-- src/vnet/session/session.c | 4 +++- src/vnet/udp/udp_input.c | 19 ++++++++++++++++++- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/plugins/srtp/srtp.c b/src/plugins/srtp/srtp.c index f388591d5ea3..b260beb04575 100644 --- a/src/plugins/srtp/srtp.c +++ b/src/plugins/srtp/srtp.c @@ -482,8 +482,13 @@ srtp_migrate_ctx (void *arg) us->opaque = ctx_handle; /* Migrate app session as well */ - session_dgram_connect_notify (&ctx->connection, ctx->app_session_handle, &new_app_session); - ctx->app_session_handle = session_handle (new_app_session); + if (session_dgram_connect_notify (&ctx->connection, ctx->app_session_handle, + &new_app_session) < 0) + clib_warning ("srtp: could not migrate app session of udp session 0x%lx", + ctx->srtp_session_handle); + else + ctx->app_session_handle = session_handle (new_app_session); + us = session_get_from_handle (ctx->srtp_session_handle); session_migrate_accept (us); diff --git a/src/vnet/session/session.c b/src/vnet/session/session.c index 464b90206556..942c74c5cb5b 100644 --- a/src/vnet/session/session.c +++ b/src/vnet/session/session.c @@ -962,8 +962,10 @@ session_dgram_connect_notify (transport_connection_t *tc, attach_error: if (rx_attached) segment_manager_detach_fifo (&new_s->rx_fifo); + /* The entry was pointed at new_s above: give it back to the session that + * stays in use */ if (!(tc->flags & TRANSPORT_CONNECTION_F_NO_LOOKUP)) - session_lookup_del_connection (tc); + session_lookup_add_connection (tc, osh.handle); session_free (new_s); return -1; } diff --git a/src/vnet/udp/udp_input.c b/src/vnet/udp/udp_input.c index a0bee693481b..a96647f7e430 100644 --- a/src/vnet/udp/udp_input.c +++ b/src/vnet/udp/udp_input.c @@ -324,7 +324,24 @@ udp46_input_inline (vlib_main_t *vm, vlib_node_runtime_t *node, /* * Ask session layer for a new session. */ - session_dgram_connect_notify (&uc0->connection, osh, &s0); + if (PREDICT_FALSE (session_dgram_connect_notify ( + &uc0->connection, osh, &s0) < 0)) + { + /* + * The migration was rolled back and s0 still belongs to + * another thread. Undo the transport clone, hand the + * datagram to the session's own thread and let it notify + * the application. + */ + udp_connection_t *old_uc = udp_connection_from_transport ( + session_get_transport (s0)); + old_uc->flags &= ~UDP_CONN_F_MIGRATED; + udp_connection_enqueue (uc0, s0, &hdr0, thread_index, b[0], + 0 /* queue_event */, &error0); + session_program_rx_io_evt (osh); + udp_connection_free (uc0); + goto done; + } queue_event = 0; } else From af7f7325a1487200551751dfb102659ac7ed41e0 Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 22 Sep 2026 05:18:44 +0800 Subject: [PATCH 56/85] vapi, memif: fix failure-path leaks (#11) * vapi: free the socket on a failed socket connect The failure path closed the socket and zeroed the structure, losing the receive buffer that vapi_sock_recv_internal() allocates and leaking it on every failed connect. Use clib_socket_free(), as the disconnect path does. Type: fix * memif: free the buffers behind rejected zero-copy descriptors memif_zc_rx_packet_valid() rejects a descriptor chain, and the input node skips its slots without returning their buffers. The refill then overwrites the ring's copy of those buffer indices, so the buffers never return to the pool. Free them while skipping. Type: fix * memif: only consume descriptors that were published to the peer The zero-copy RX path processes the slots between mq->last_tail and ring->tail, and ring->tail is written by the peer. The buffer indices in mq->buffers[] are only valid for the slots that were published to the peer, so a peer that advertises more than it was given makes the path use stale indices, and for the descriptor chains rejected by memif_zc_rx_packet_valid() it now also frees them. Clamp the range to the published slots, which is what the non zero-copy path guards its parsed descriptor count against. Type: fix --- src/plugins/memif/node.c | 19 +++++++++++++++++++ src/vpp-api/vapi/vapi.c | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/plugins/memif/node.c b/src/plugins/memif/node.c index b08ab02c49f5..35e3b2735a32 100644 --- a/src/plugins/memif/node.c +++ b/src/plugins/memif/node.c @@ -813,6 +813,17 @@ memif_device_input_zc_inline (vlib_main_t *vm, vlib_node_runtime_t *node, goto done; } + /* ring->tail is written by the peer: only the slots that were published to + * it hold a valid buffer index in mq->buffers[] */ + head = __atomic_load_n (&ring->head, __ATOMIC_ACQUIRE); + if (PREDICT_FALSE (n_slots > (u16) (head - cur_slot))) + { + vlib_error_count (vm, node->node_index, MEMIF_INPUT_ERROR_BAD_DESC, 1); + n_slots = head - cur_slot; + if (n_slots == 0) + goto refill; + } + /* process ring slots */ vec_validate_aligned (ptd->buffers, MEMIF_RX_VECTOR_SZ, CLIB_CACHE_LINE_BYTES); @@ -824,6 +835,14 @@ memif_device_input_zc_inline (vlib_main_t *vm, vlib_node_runtime_t *node, if (PREDICT_FALSE (!memif_zc_rx_packet_valid ( ring, cur_slot, n_slots, mask, buffer_length, &packet_slots))) { + u16 slot_i; + + /* These descriptors never reach the graph and the refill below + overwrites the ring's copy of their buffer indices, so return the + buffers here. */ + for (slot_i = 0; slot_i < packet_slots; slot_i++) + vlib_buffer_free_one (vm, mq->buffers[(cur_slot + slot_i) & mask]); + cur_slot += packet_slots; n_slots -= packet_slots; vlib_error_count (vm, node->node_index, MEMIF_INPUT_ERROR_BAD_DESC, diff --git a/src/vpp-api/vapi/vapi.c b/src/vpp-api/vapi/vapi.c index 5a999debf55c..1ce722ee915a 100644 --- a/src/vpp-api/vapi/vapi.c +++ b/src/vpp-api/vapi/vapi.c @@ -811,7 +811,7 @@ vapi_sock_client_connect (vapi_ctx_t ctx, char *path, const char *name) fail: clib_socket_close (sock); - clib_memset (sock, 0, sizeof (*sock)); + clib_socket_free (sock); vec_free (msg); return rv; } From e21c683b9b38650717da444f6609ce1c9c952747 Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 22 Sep 2026 05:49:14 +0800 Subject: [PATCH 57/85] teib: drop the peer/next-hop address family check (#12) teib_entry_add() rejected an entry whose peer and next hop used different address families. Those addresses live on different sides of the tunnel: the next hop is in the underlay that carries the tunnel, the peer is in the overlay. A p2mp GRE tunnel under an IPv4 underlay uses an IPv6 link-local peer, which is a valid configuration and is exercised by test_ip6.test_ip6_ll_p2mp. Remove the check and turn the fork test that asserted the rejection into a positive test that programs such a tunnel and verifies traffic. Type: fix Change-Id: I0000000000000000000000000000000000000001 Signed-off-by: Bo Xu --- src/vnet/teib/teib.c | 3 --- test/test_gre.py | 43 ++++++++++++++++++++++++++++++------------- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/src/vnet/teib/teib.c b/src/vnet/teib/teib.c index 84d765d1501f..6fa3167b8503 100644 --- a/src/vnet/teib/teib.c +++ b/src/vnet/teib/teib.c @@ -189,9 +189,6 @@ teib_entry_add (u32 sw_if_index, u32 nh_fib_index, peer_fib_index; index_t tei; - if (ip_addr_version (nh) != ip_addr_version (peer)) - return (VNET_API_ERROR_INVALID_ADDRESS_FAMILY); - nh_proto = (AF_IP4 == ip_addr_version (nh) ? FIB_PROTOCOL_IP4 : FIB_PROTOCOL_IP6); diff --git a/test/test_gre.py b/test/test_gre.py index e52ec3fe9cfc..881a45a64f10 100644 --- a/test/test_gre.py +++ b/test/test_gre.py @@ -6,7 +6,7 @@ from scapy.packet import Raw from scapy.layers.l2 import Ether, Dot1Q, GRE from scapy.layers.inet import IP, UDP -from scapy.layers.inet6 import IPv6 +from scapy.layers.inet6 import IPv6, ICMPv6EchoRequest from scapy.volatile import RandMAC, RandIP from framework import VppTestCase @@ -19,6 +19,7 @@ VppIpRoute, VppRoutePath, VppIpTable, + VppIpInterfaceAddress, VppMplsLabel, ) from vpp_mpls_tunnel_interface import VppMPLSTunnelInterface @@ -870,7 +871,8 @@ def test_non_erspan_session_id_is_rejected(self): }, ) - def test_teib_next_hop_family_matches_gre_underlay(self): + def test_teib_ipv6_peer_over_ipv4_underlay(self): + """a TEIB peer may use a different family than its next hop""" e = VppEnum.vl_api_tunnel_mode_t gre_if = VppGreInterface( self, @@ -882,18 +884,33 @@ def test_teib_next_hop_family_matches_gre_underlay(self): gre_if.admin_up() gre_if.config_ip4() - with self.vapi.assert_negative_api_retval(): - self.vapi.teib_entry_add_del( - is_add=1, - entry={ - "nh_table_id": 0, - "sw_if_index": gre_if.sw_if_index, - "peer": self.pg3.remote_ip4, - "nh": self.pg2.remote_ip6, - }, - ) + ll1 = "fe80:1::1" + ll2 = "fe80:2::2" + + VppIpInterfaceAddress(self, gre_if, ll1, 128).add_vpp_config() + + p_echo_request = ( + Ether(src=self.pg3.remote_mac, dst=self.pg3.local_mac) + / IP(src=self.pg3.remote_ip4, dst=self.pg3.local_ip4) + / GRE() + / IPv6(src=ll2, dst=ll1) + / ICMPv6EchoRequest() + ) + + # the tunnel has no peer in the link-local table yet + self.send_and_assert_no_replies(self.pg3, [p_echo_request]) + + # the peer is IPv6 link-local, the next hop is IPv4 + teib = VppTeib(self, gre_if, ll2, self.pg3.remote_ip4) + teib.add_vpp_config() + self.assertTrue(teib.query_vpp_config()) + + self.logger.info(self.vapi.cli("sh ip6-ll %s %s" % (gre_if.name, ll2))) + self.send_and_expect(self.pg3, [p_echo_request], self.pg3) - gre_if.unconfig_ip4() + # the tunnel is gone, so is its teib entry; the registry removes the + # remaining objects in reverse order + teib.remove_vpp_config() gre_if.admin_down() gre_if.remove_vpp_config() From e6695a6f036755c35e90850df59bb8b7e9b2e67b Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 22 Sep 2026 06:15:44 +0800 Subject: [PATCH 58/85] flowprobe: reject an unknown value of which on the replaced tx message (#13) The datapath of flowprobe_interface_add_del_feature() takes a flowprobe_variant_t, but vl_api_flowprobe_tx_interface_add_del_t carries a bitmask of forwarding paths (IP4 0x1, L2 0x2, IP6 0x4) and the handler passes the field through unchanged. A value above FLOW_VARIANT_L2 names no variant, so flowprobe_get_template_specs() returns no template for it and the value is then used to index the per-variant arrays, including the flush_completed_generation_per_worker and context arrays added to flowprobe_main_t. Validate the field in the handler the way the replacement handler does. Type: fix Change-Id: I0000000000000000000000000000000000000002 Signed-off-by: Bo Xu --- src/plugins/flowprobe/flowprobe.api | 2 +- src/plugins/flowprobe/flowprobe.c | 13 +++++++++++++ test/test_flowprobe.py | 18 ++++++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/plugins/flowprobe/flowprobe.api b/src/plugins/flowprobe/flowprobe.api index c2090637cc82..d659da930b01 100644 --- a/src/plugins/flowprobe/flowprobe.api +++ b/src/plugins/flowprobe/flowprobe.api @@ -41,7 +41,7 @@ enum flowprobe_direction : u8 @param client_index - opaque cookie to identify the sender @param context - sender context, to match reply w/ request @param is_add - add address if non-zero, else delete - @param which - flags indicating forwarding path + @param which - datapath on which to record flows (flowprobe_which_flags_t) @param sw_if_index - index of the interface */ autoreply define flowprobe_tx_interface_add_del diff --git a/src/plugins/flowprobe/flowprobe.c b/src/plugins/flowprobe/flowprobe.c index 154a54f1bb88..bea1bc1c7326 100644 --- a/src/plugins/flowprobe/flowprobe.c +++ b/src/plugins/flowprobe/flowprobe.c @@ -936,6 +936,19 @@ void vl_api_flowprobe_tx_interface_add_del_t_handler VALIDATE_SW_IF_INDEX (mp); + /* + * The field is documented as a bitmask of forwarding paths and typed as one, + * but it is passed to the datapath as a flowprobe_variant_t. The two sets + * of values only overlap on 0, 1 and 2; the rest either name a variant this + * message cannot reach or fall outside the variant arrays, so reject them. + */ + if ((u8) mp->which > (u8) FLOW_VARIANT_L2) + { + clib_warning ("Invalid value of which"); + rv = VNET_API_ERROR_INVALID_VALUE; + goto out; + } + if (fm->record == 0) { clib_warning ("Please specify flowprobe params record first..."); diff --git a/test/test_flowprobe.py b/test/test_flowprobe.py index c9219ca2b770..cfddafe649dc 100644 --- a/test/test_flowprobe.py +++ b/test/test_flowprobe.py @@ -679,6 +679,24 @@ def test_get_params(self): ipfix.remove_vpp_config() self.logger.info("FFP_TEST_FINISH_0004") + def test_tx_interface_add_del_invalid_which(self): + """the replaced tx message rejects a which that names no variant""" + self.logger.info("FFP_TEST_START_TX_WHICH") + + # the message used to carry a bitmask of forwarding paths; 3 is + # FLOWPROBE_WHICH_FLAG_IP4 | FLOWPROBE_WHICH_FLAG_L2 and names no + # variant, while 5 is past the end of the variant arrays + for which in (3, 5): + with self.subTest(which=which): + with self.vapi.assert_negative_api_retval(): + self.vapi.flowprobe_tx_interface_add_del( + is_add=1, + which=which, + sw_if_index=self.pg0.sw_if_index, + ) + + self.logger.info("FFP_TEST_FINISH_TX_WHICH") + class DatapathTestsHolder(object): """collect information on Ethernet, IP4 and IP6 datapath (no timers)""" From ae0bf748e1b9852477677e245f4486318aab266a Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 22 Sep 2026 09:29:53 +0800 Subject: [PATCH 59/85] github: build the deb from the commit the run is for (#14) Three defects in the Trixie deb workflow: - git clone --depth 1 --branch $GITHUB_REF_NAME takes the branch tip at clone time, not the commit the run is attributed to. Run 35656388037 is attributed to 6d8c17601 and its archived debs are versioned ~gaf7f732, a descendant commit, so the artifact does not match the run or the release body. Check out $GITHUB_SHA after the clone, fetching just that commit when the tip has moved. - git describe --tags --long --match 'v*' also matches this workflow's own deb release tags (vpp-trixie-), which leaves src/scripts/.version unwritten and sends src/scripts/version to its own git describe. That one ignores lightweight tags, finds nothing, and exits 1, so the package version comes out empty. Match 'v[0-9]*' instead. - Neither cache entry could be reused as written. The apt archive cache asked to save under ${{ github.run_id }}, a key no later run can ask for, so every run stored another copy; the ccache entry was keyed by ref alone, so only the first build of a ref could save it and the stored ccache never picked up newer objects. The apt cache is now keyed by the Makefile, which defines $(DEB_DEPENDS), the package list "make install-dep" installs, and the hash is taken in a step of its own ahead of the install, because that install rewrites the Makefile: a hashFiles expression would hand the save step a different key than the restore step asked for. The ccache is keyed by commit, so each build has a fresh entry to save. The restore-keys prefixes still lend a run the newest entry when it has none of its own. The synthesised version string is left alone. Gluing the hash to the commit timestamp looks like it would order two builds of the same second by hash, but Debian compares digit runs by length before value, so the glued form lets a hash that starts with a digit outrank every later build; the "-g" separator keeps the timestamp and the hash in separate runs and the timestamp decides first. Type: fix Change-Id: I0000000000000000000000000000000000000003 Signed-off-by: Bo Xu --- .github/workflows/build-deb-trixie.yml | 65 +++++++++++++++++++++----- 1 file changed, 53 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build-deb-trixie.yml b/.github/workflows/build-deb-trixie.yml index 753cc19ac03d..31daac5f5f51 100644 --- a/.github/workflows/build-deb-trixie.yml +++ b/.github/workflows/build-deb-trixie.yml @@ -28,14 +28,6 @@ jobs: - name: Disable APT cache cleanup run: rm -f /etc/apt/apt.conf.d/docker-clean - - name: Restore apt archive cache - uses: actions/cache/restore@v4 - with: - path: /var/cache/apt/archives - key: apt-debian13-${{ runner.arch }}-${{ github.run_id }} - restore-keys: | - apt-debian13-${{ runner.arch }}- - - name: Install base tools run: | apt-get update -qq @@ -45,6 +37,37 @@ jobs: run: | git clone --depth 1 --branch "$GITHUB_REF_NAME" \ "https://github.com/$GITHUB_REPOSITORY.git" . + # The branch can move between the event and this clone, so check out + # the commit the run is for instead of whatever the branch points at + # now; otherwise the debs and their version string would name a + # different commit than the run and the release body do. + git config --global --add safe.directory "$PWD" + if [ "$(git rev-parse HEAD)" != "$GITHUB_SHA" ]; then + git fetch --no-tags --depth 1 origin "$GITHUB_SHA" + git checkout --detach FETCH_HEAD + fi + + # "make install-dep" below is the only step that reads this cache, and + # the package list it installs, $(DEB_DEPENDS), is defined in the + # Makefile. Key the cache by that file, so the key moves when the + # package list moves: the run that first needs a new package is then the + # run that stores it. Hash it into an output rather than with + # hashFiles, because the install step rewrites the Makefile between the + # restore and the save and both steps have to ask for the same key. + # Reading the file at all needs the clone, and the base tools install + # above runs before this point, so it reads no cache. + - name: Hash the dependency list + id: deps + shell: bash + run: echo "hash=$(git hash-object Makefile)" >> $GITHUB_OUTPUT + + - name: Restore apt archive cache + uses: actions/cache/restore@v4 + with: + path: /var/cache/apt/archives + key: apt-debian13-${{ runner.arch }}-${{ steps.deps.outputs.hash }} + restore-keys: | + apt-debian13-${{ runner.arch }}- - name: Compute short SHA id: vars @@ -63,21 +86,34 @@ jobs: uses: actions/cache/save@v4 with: path: /var/cache/apt/archives - key: apt-debian13-${{ runner.arch }}-${{ github.run_id }} + key: apt-debian13-${{ runner.arch }}-${{ steps.deps.outputs.hash }} - name: Restore ccache uses: actions/cache/restore@v4 with: path: build-root/.ccache - key: ccache-debian13-${{ runner.arch }}-${{ github.ref_name }} + # Keyed by commit, not by ref alone. With a fixed key only the + # first build of a ref could save this entry; every later build + # asked to save a key that was already stored, so the ccache stayed + # at whatever the first build left in it. A commit key gives each + # build a fresh entry to save, and the prefix in restore-keys still + # lends a run the newest entry of another commit on the same ref. + key: ccache-debian13-${{ runner.arch }}-${{ github.ref_name }}-${{ github.sha }} restore-keys: | + ccache-debian13-${{ runner.arch }}-${{ github.ref_name }}- ccache-debian13-${{ runner.arch }}- - name: Build VPP DEB packages run: | git config --global --add safe.directory "$PWD" git fetch --tags --force 2>/dev/null || true - if ! git describe --tags --long --match 'v*' 2>/dev/null; then + # Only a VPP release tag (vYY.MM...) should win over the synthesised + # version. A bare 'v*' also matches this workflow's own deb release + # tags (vpp-trixie-), which would leave .version unwritten and + # send src/scripts/version to its own git describe; that one does not + # look at lightweight tags, so it would find none and exit 1, leaving + # the package version empty. + if ! git describe --tags --long --match 'v[0-9]*' 2>/dev/null; then # No matching tag in this repo, so synthesise one. The build # identifier must vary per commit, otherwise every build of this # branch produces the same version string and apt skips packages @@ -92,6 +128,11 @@ jobs: # # Use the commit timestamp rather than git rev-list --count: # this job clones with --depth 1, where the count is always 1. + # Keep the separator in front of the hash: src/scripts/version + # turns it into "~g" in the package version. Glued to the + # timestamp the hash joins the same digit run, and Debian compares + # digit runs by length before value, so a build whose hash starts + # with a digit outranks every later build with a shorter run. echo "0.0.0-release+$(git log -1 --format=%ct)-g$(git rev-parse --short=7 HEAD)" \ > src/scripts/.version fi @@ -106,7 +147,7 @@ jobs: uses: actions/cache/save@v4 with: path: build-root/.ccache - key: ccache-debian13-${{ runner.arch }}-${{ github.ref_name }} + key: ccache-debian13-${{ runner.arch }}-${{ github.ref_name }}-${{ github.sha }} - name: Upload DEB artifacts uses: actions/upload-artifact@v4 From 98b1a9a308b916e34b14ad8c815bd681115101aa Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 22 Sep 2026 09:30:35 +0800 Subject: [PATCH 60/85] api: copy traced messages in full (#15) A message whose length depends on its contents has to be traced whole. The stored copy is walked by the endian, format and json handlers, which follow the element count field inside the copy: cutting the copy short keeps the count, and they then read and write past the end of the copy. m->trace_size is not a bound for the copy. It sizes the initializer array that "api trace initializers" prints (src/vlibmemory/vlib_api_cli.c), and that is the only reader the value had before this fork's change; upstream copied the whole message. Messages the cut hit: - sr_mpls_policy_add and sr_mpls_policy_mod register a trace size of 256 (src/plugins/srmpls/sr_mpls_api.c), while 100 segments put 420 and 427 bytes on the wire; - geneve_add_del_tunnel raises its trace size to 64 (src/plugins/geneve/geneve_api.c), while the message is 67 bytes. Reproduce: turn the api trace on, send sr_mpls_policy_add with 100 segments, then "api trace save-json". Before this change vpp aborts in cJSON_CreateObject, called from the srmpls plugin json handler. test/asf/test_api_trace.py sends that request and reads the segments back out of the saved json and out of "api trace dump-json". --- src/vlibapi/api_shared.c | 11 ++++++++--- test/asf/test_api_trace.py | 37 ++++++++++++++++++++++++++++++++----- 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/src/vlibapi/api_shared.c b/src/vlibapi/api_shared.c index 3a83fe290d97..9e57374f2ffd 100644 --- a/src/vlibapi/api_shared.c +++ b/src/vlibapi/api_shared.c @@ -77,10 +77,15 @@ vl_msg_api_trace_with_size (api_main_t *am, vl_api_trace_t *tp, void *msg, if (!m || !m->trace_enable) return; + /* + * Copy the whole message. msg_len is the size of the message buffer, and + * a message with a variable length array member must be copied in full: a + * shorter copy keeps the larger element count, so the endian, format and + * json handlers read and write past the end of the copy. m->trace_size is + * not a bound for the copy, it sizes the initializer array printed by + * "api trace initializers". + */ length = msg_len; - if (m->trace_size >= (int) sizeof (u16) && - length > (uword) m->trace_size) - length = m->trace_size; msg_copy = 0; diff --git a/test/asf/test_api_trace.py b/test/asf/test_api_trace.py index 04fcf63ca411..1f997d258eb2 100644 --- a/test/asf/test_api_trace.py +++ b/test/asf/test_api_trace.py @@ -20,17 +20,19 @@ def setUp(self): def tearDownClass(cls): super(TestJsonApiTrace, cls).tearDownClass() - def test_json_api_trace_save(self): - self.vapi.show_version() - + def save_json_trace(self): fname = "test_api_trace-%d.json" % self.vpp.pid tmp_api_trace = "/tmp/%s" % fname fpath = "%s/%s" % (self.tempdir, fname) self.vapi.cli("api trace save-json {}".format(fname)) shutil.move(tmp_api_trace, fpath) with open(fpath, encoding="utf-8") as f: - s = f.read() - trace = json.loads(s) + return json.load(f) + + def test_json_api_trace_save(self): + self.vapi.show_version() + + trace = self.save_json_trace() found = False for o in trace: if o["_msgname"] == "show_version": @@ -39,6 +41,31 @@ def test_json_api_trace_save(self): self.assertTrue(found) self.assertEqual(o["_msgname"], "show_version") + def test_json_api_trace_variable_length_message(self): + """A message with a variable length array is traced and saved whole""" + n_segments = 100 + # The request is traced when it is received, the reply is of no + # interest here, so call the API without the return value check. The + # stored copy has to hold all the segments: a copy cut short keeps the + # element count and the dump walks off the end of it. + self.vapi.papi.sr_mpls_policy_add( + bsid=1, + weight=1, + is_spray=False, + n_segments=n_segments, + segments=list(range(100, 100 + n_segments)), + ) + + trace = self.save_json_trace() + o = [e for e in trace if e["_msgname"] == "sr_mpls_policy_add"][0] + self.assertEqual(o["n_segments"], n_segments) + self.assertEqual(o["segments"], list(range(100, 100 + n_segments))) + + # dumping walks the same arrays as saving + dump = self.vapi.cli("api trace dump-json") + self.assertIn("sr_mpls_policy_add", dump) + self.assertIn("segments", dump) + def test_json_api_trace_replay(self): fname = "/tmp/create_loop.json" req = """ From 060db0fc5c1b35996f8128842b1a14c474175db4 Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 22 Sep 2026 10:26:21 +0800 Subject: [PATCH 61/85] github: break deb version ties inside the same second (#16) The synthesised package version carried only the commit timestamp, so two commits made in the same second produced versions that differ only in the trailing hash. Debian compares those hashes as text, which carries no order: the child could sort below its parent, and apt would then keep the older deb. Append the commit count, which grows strictly along the parent chain and decides the comparison before the hash is reached. Counting commits needs the full history, so unshallow the clone the job starts from, and warn if that fetch fails instead of silently packaging a constant count. The release body read its build date from github.event.head_commit, which workflow_dispatch does not carry, so the field rendered empty. Neither does the github context expose a run start time; have the runner record the time. --- .github/workflows/build-deb-trixie.yml | 29 +++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-deb-trixie.yml b/.github/workflows/build-deb-trixie.yml index 31daac5f5f51..a1823187e224 100644 --- a/.github/workflows/build-deb-trixie.yml +++ b/.github/workflows/build-deb-trixie.yml @@ -69,12 +69,16 @@ jobs: restore-keys: | apt-debian13-${{ runner.arch }}- - - name: Compute short SHA + - name: Compute short SHA and build date id: vars shell: bash run: | git config --global --add safe.directory "$PWD" echo "short_sha=$(git rev-parse --short=7 HEAD)" >> $GITHUB_OUTPUT + # The release body used github.event.head_commit.timestamp, which a + # workflow_dispatch run does not carry; the github context has no run + # start time either, so record one here. + echo "date=$(date -u '+%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT - name: Install VPP build dependencies run: | @@ -107,6 +111,15 @@ jobs: run: | git config --global --add safe.directory "$PWD" git fetch --tags --force 2>/dev/null || true + # The version below counts the commits in the history, so it needs + # the whole history; the clone above is shallow. Asking for the + # unshallow only when the history is short keeps a full repository + # from erroring out, and the warning keeps a failure here from + # quietly turning the count into a constant 1. + if [ "$(git rev-parse --is-shallow-repository)" = true ]; then + git fetch --unshallow --quiet || + echo "::warning::full history unavailable, the commit count in the version is not reliable" + fi # Only a VPP release tag (vYY.MM...) should win over the synthesised # version. A bare 'v*' also matches this workflow's own deb release # tags (vpp-trixie-), which would leave .version unwritten and @@ -126,14 +139,20 @@ jobs: # outrank digits) and apt would refuse it. Extending the existing # "-release" revision keeps every new build strictly newer. # - # Use the commit timestamp rather than git rev-list --count: - # this job clones with --depth 1, where the count is always 1. + # The commit timestamp alone does not order two commits made in + # the same second: Debian then falls through to the hash, which + # carries no order, so the child can sort below its parent and apt + # keeps the older deb. Append the commit count, which grows + # strictly along the parent chain. Counting commits is why the + # fetch above has to unshallow the clone; at --depth 1 the count + # is 1 for every commit. + # # Keep the separator in front of the hash: src/scripts/version # turns it into "~g" in the package version. Glued to the # timestamp the hash joins the same digit run, and Debian compares # digit runs by length before value, so a build whose hash starts # with a digit outranks every later build with a shorter run. - echo "0.0.0-release+$(git log -1 --format=%ct)-g$(git rev-parse --short=7 HEAD)" \ + echo "0.0.0-release+$(git log -1 --format=%ct).$(git rev-list --count HEAD)-g$(git rev-parse --short=7 HEAD)" \ > src/scripts/.version fi ccache -z @@ -165,6 +184,6 @@ jobs: body: | VPP build from commit ${{ github.sha }} - Branch: ${{ github.ref_name }} - - Build date: ${{ github.event.head_commit.timestamp }} + - Build date: ${{ steps.vars.outputs.date }} draft: false prerelease: true From 46c1e4ed87969516925edd21ef40e3036a74982a Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 22 Sep 2026 10:59:19 +0800 Subject: [PATCH 62/85] ipsec: unwind a refused tunnel protection; gre: bound the tunnel lookup (#17) * gre: look up a tunnel through one bounded helper gre_tunnel_desc indexed the sw_if_index to tunnel mapping without checking the index against the length of that mapping. IPsec tunnel protection resolves the device class from the super interface of the interface it is given and then passes that interface's sw_if_index, so protecting a sub-interface of a TEB tunnel indexed the mapping past its end. The read returned whatever followed the vector, and because that value is not the ~0 sentinel, IPsec accepted the configuration and took the tunnel addresses from another tunnel's slot or from unallocated memory. gre_build_rewrite, gre_update_adj, mgre_update_adj and gre_tunnel_desc each repeated the same lookup, two of them guarded and two not. Put the lookup in one helper that checks the index, the sentinel and the pool free bitmap, and call it from all four. * ipsec: give back what a refused tunnel protection took ipsec_tun_protect_update locks the output SA and the input SAs before it validates the interface, and every error exit returned without unlocking them. The lock count then never reaches zero, so the SA stays in the SAD: ipsec_sad_entry_del reports success but leaves the entry in the dump, and the SA cannot be removed short of a restart. The API reaches this by asking to protect an interface whose device class protects no tunnel (any non-tunnel interface), or a sub-interface of a GRE or IPIP tunnel, which those plugins refuse. The same exits left the freshly allocated ipsec_tun_protect_t and its key in the pool, where the walk behind "show ipsec protect" lists them. Release the locks and the entry on the way out. Only the first n_sa_in_locked entries of sas_in hold locked SA indices, so the entries the caller still owns are left untouched and the vector stays with the caller, as the callers that free it themselves on error expect. --- src/plugins/gre/gre.c | 64 +++++++++++++++++++++----------------- src/vnet/ipsec/ipsec_tun.c | 33 ++++++++++++++++++++ test/test_gre.py | 49 +++++++++++++++++++++++++++++ test/test_ipsec_api.py | 31 ++++++++++++++++++ 4 files changed, 149 insertions(+), 28 deletions(-) diff --git a/src/plugins/gre/gre.c b/src/plugins/gre/gre.c index 6d5c22742353..b8a98784ed3e 100644 --- a/src/plugins/gre/gre.c +++ b/src/plugins/gre/gre.c @@ -194,29 +194,51 @@ gre_proto_from_vnet_link (vnet_link_t link) return (GRE_PROTOCOL_ip4); } +/* + * The mapping holds an entry for the tunnel's own interface only, so a + * sub-interface of a tunnel is not one of ours; indexing the vector with such + * an index reads past its end and has to be rejected first. + */ +static gre_tunnel_t * +gre_tunnel_find_by_sw_if_index (u32 sw_if_index) +{ + gre_main_t *gm = &gre_main; + u32 ti; + + if (vec_len (gm->tunnel_index_by_sw_if_index) <= sw_if_index) + return (0); + + ti = gm->tunnel_index_by_sw_if_index[sw_if_index]; + + if (~0 == ti) + /* not one of ours */ + return (0); + + if (pool_is_free_index (gm->tunnels, ti)) + return (0); + + return (pool_elt_at_index (gm->tunnels, ti)); +} + static u8 * gre_build_rewrite (vnet_main_t *vnm, u32 sw_if_index, vnet_link_t link_type, const void *dst_address) { - gre_main_t *gm = &gre_main; const ip46_address_t *dst; ip4_and_gre_header_t *h4; ip6_and_gre_header_t *h6; gre_header_t *gre; u8 *rewrite = NULL; gre_tunnel_t *t; - u32 ti; u8 is_ipv6; dst = dst_address; - ti = gm->tunnel_index_by_sw_if_index[sw_if_index]; + t = gre_tunnel_find_by_sw_if_index (sw_if_index); - if (~0 == ti) + if (0 == t) /* not one of ours */ return (0); - t = pool_elt_at_index (gm->tunnels, ti); - is_ipv6 = t->tunnel_dst.fp_proto == FIB_PROTOCOL_IP6 ? 1 : 0; if (!is_ipv6) @@ -402,19 +424,14 @@ gre_get_fixup (fib_protocol_t fproto, vnet_link_t lt) void gre_update_adj (vnet_main_t *vnm, u32 sw_if_index, adj_index_t ai) { - gre_main_t *gm = &gre_main; gre_tunnel_t *t; adj_flags_t af; - u32 ti; - if ((vec_len (gm->tunnel_index_by_sw_if_index) <= sw_if_index) || - (~0 == gm->tunnel_index_by_sw_if_index[sw_if_index]) || - pool_is_free_index (gm->tunnels, - gm->tunnel_index_by_sw_if_index[sw_if_index])) + t = gre_tunnel_find_by_sw_if_index (sw_if_index); + + if (0 == t) return; - ti = gm->tunnel_index_by_sw_if_index[sw_if_index]; - t = pool_elt_at_index (gm->tunnels, ti); af = ADJ_FLAG_NONE; /* @@ -479,21 +496,16 @@ mgre_mk_incomplete_walk (adj_index_t ai, void *data) void mgre_update_adj (vnet_main_t *vnm, u32 sw_if_index, adj_index_t ai) { - gre_main_t *gm = &gre_main; ip_adjacency_t *adj; teib_entry_t *ne; gre_tunnel_t *t; - u32 ti; - if ((vec_len (gm->tunnel_index_by_sw_if_index) <= sw_if_index) || - (~0 == gm->tunnel_index_by_sw_if_index[sw_if_index]) || - pool_is_free_index (gm->tunnels, - gm->tunnel_index_by_sw_if_index[sw_if_index])) + t = gre_tunnel_find_by_sw_if_index (sw_if_index); + + if (0 == t) return; adj = adj_get (ai); - ti = gm->tunnel_index_by_sw_if_index[sw_if_index]; - t = pool_elt_at_index (gm->tunnels, ti); ne = teib_entry_find_46 (sw_if_index, adj->ia_nh_proto, &adj->sub_type.nbr.next_hop); @@ -740,18 +752,14 @@ static int gre_tunnel_desc (u32 sw_if_index, ip46_address_t *src, ip46_address_t *dst, u8 *is_l2) { - gre_main_t *gm = &gre_main; gre_tunnel_t *t; - u32 ti; - ti = gm->tunnel_index_by_sw_if_index[sw_if_index]; + t = gre_tunnel_find_by_sw_if_index (sw_if_index); - if (~0 == ti) + if (0 == t) /* not one of ours */ return -1; - t = pool_elt_at_index (gm->tunnels, ti); - *src = t->tunnel_src; *dst = t->tunnel_dst.fp_addr; *is_l2 = t->type == GRE_TUNNEL_TYPE_TEB; diff --git a/src/vnet/ipsec/ipsec_tun.c b/src/vnet/ipsec/ipsec_tun.c index b2d4227bad3b..25498129e89a 100644 --- a/src/vnet/ipsec/ipsec_tun.c +++ b/src/vnet/ipsec/ipsec_tun.c @@ -557,10 +557,17 @@ ipsec_tun_protect_update (u32 sw_if_index, const ip_address_t * nh, u32 sa_out, u32 * sas_in) { ipsec_tun_protect_t *itp; + ipsec_tun_protect_t *itp_new; u32 itpi, ii, *saip; + u32 n_sa_in_locked; ipsec_main_t *im; + u8 sa_out_locked; int rv; + itp_new = 0; + n_sa_in_locked = 0; + sa_out_locked = 0; + if (NULL == nh) nh = &IP_ADDR_ALL_0; @@ -584,10 +591,12 @@ ipsec_tun_protect_update (u32 sw_if_index, if (~0 == sas_in[ii]) { rv = VNET_API_ERROR_INVALID_VALUE; + n_sa_in_locked = ii; goto out; } } + n_sa_in_locked = vec_len (sas_in); sa_out = ipsec_sa_find_and_lock (sa_out); if (~0 == sa_out) @@ -596,6 +605,8 @@ ipsec_tun_protect_update (u32 sw_if_index, goto out; } + sa_out_locked = 1; + if (INDEX_INVALID == itpi) { vnet_device_class_t *dev_class; @@ -614,6 +625,7 @@ ipsec_tun_protect_update (u32 sw_if_index, } pool_get_zero (ipsec_tun_protect_pool, itp); + itp_new = itp; itp->itp_sw_if_index = sw_if_index; itp->itp_ai = ADJ_INDEX_INVALID; @@ -698,8 +710,29 @@ ipsec_tun_protect_update (u32 sw_if_index, ipsec_sa_unlock (sa_out); vec_foreach (saip, sas_in) ipsec_sa_unlock (*saip); vec_free (sas_in); + return (rv); out: + /* + * Nothing was configured, so this call may not keep what it took: a still + * locked SA can never be deleted again, and a pool entry that never + * reached the database shows up in the walk that lists the protections. + * Only the first n_sa_in_locked entries of sas_in hold locked SA indices; + * the entries after those are still the ids the caller passed, and the + * caller keeps ownership of the vector on the error paths. + */ + if (0 != itp_new) + { + clib_mem_free (itp_new->itp_key); + pool_put (ipsec_tun_protect_pool, itp_new); + } + + for (ii = 0; ii < n_sa_in_locked; ii++) + ipsec_sa_unlock (sas_in[ii]); + + if (sa_out_locked) + ipsec_sa_unlock (sa_out); + return (rv); } diff --git a/test/test_gre.py b/test/test_gre.py index 881a45a64f10..26b3b586129c 100644 --- a/test/test_gre.py +++ b/test/test_gre.py @@ -13,6 +13,7 @@ from asfframework import VppTestRunner, tag_fixme_vpp_workers from vpp_sub_interface import L2_VTR_OP, VppDot1QSubint from vpp_gre_interface import VppGreInterface +from vpp_ipsec import VppIpsecSA from vpp_teib import VppTeib from vpp_ip import DpoProto from vpp_ip_route import ( @@ -914,6 +915,54 @@ def test_teib_ipv6_peer_over_ipv4_underlay(self): gre_if.admin_down() gre_if.remove_vpp_config() + def test_ipsec_protect_rejects_tunnel_subinterface(self): + """IPsec protect on a sub-interface of a GRE tunnel is rejected""" + + gre_if = VppGreInterface( + self, + self.pg0.local_ip4, + self.pg0.remote_ip4, + type=(VppEnum.vl_api_gre_tunnel_type_t.GRE_API_TUNNEL_TYPE_TEB), + ) + gre_if.add_vpp_config() + gre_if.admin_up() + + # only the tunnel's own interface is in the lookup table, so asking + # about this one must not run off the end of the table + sub_if = VppDot1QSubint(self, gre_if, 500) + + sa_out = VppIpsecSA( + self, + 1, + 100, + VppEnum.vl_api_ipsec_integ_alg_t.IPSEC_API_INTEG_ALG_SHA1_96, + b"b" * 20, + VppEnum.vl_api_ipsec_crypto_alg_t.IPSEC_API_CRYPTO_ALG_AES_CBC_128, + b"a" * 16, + VppEnum.vl_api_ipsec_proto_t.IPSEC_API_PROTO_ESP, + ) + sa_out.add_vpp_config() + + with self.vapi.assert_negative_api_retval(): + self.vapi.ipsec_tunnel_protect_update( + tunnel={ + "sw_if_index": sub_if.sw_if_index, + "n_sa_in": 0, + "sa_out": sa_out.id, + "sa_in": [], + "nh": "0.0.0.0", + } + ) + + # the rejected call may not have recorded a protection for the + # sub-interface either + for entry in self.vapi.ipsec_tunnel_protect_dump(sw_if_index=0xFFFFFFFF): + self.assertNotEqual(entry.tun.sw_if_index, sub_if.sw_if_index) + + sub_if.remove_vpp_config() + gre_if.admin_down() + gre_if.remove_vpp_config() + def test_gre6(self): """GRE IPv6 tunnel Tests""" diff --git a/test/test_ipsec_api.py b/test/test_ipsec_api.py index 11dc4dd0c9c8..3310bc080750 100644 --- a/test/test_ipsec_api.py +++ b/test/test_ipsec_api.py @@ -44,6 +44,37 @@ def __sa_dump(self, sa): return dump self.fail("SA not found in VPP") + def test_rejected_protect_gives_the_sa_back(self): + """a rejected tunnel protection must not pin the SA""" + sa_out = VppIpsecSA( + self, + self.ipv4_params.scapy_tun_sa_id, + self.ipv4_params.scapy_tun_spi, + self.ipv4_params.auth_algo_vpp_id, + self.ipv4_params.auth_key, + self.ipv4_params.crypt_algo_vpp_id, + self.ipv4_params.crypt_key, + self.vpp_esp_protocol, + ) + sa_out.add_vpp_config() + + # pg0's device class protects no tunnel, so this is refused + with self.vapi.assert_negative_api_retval(): + self.vapi.ipsec_tunnel_protect_update( + tunnel={ + "sw_if_index": self.pg0.sw_if_index, + "n_sa_in": 0, + "sa_out": sa_out.id, + "sa_in": [], + "nh": "0.0.0.0", + } + ) + + # the refused call must have released the SA, or it stays locked and + # can never be deleted again + sa_out.remove_vpp_config() + self.assertFalse(sa_out.query_vpp_config()) + def test_sa_basic(self): """basic SA API tests""" sa = VppIpsecSA( From 61f1b68740157c0c45da21fe8d965c9dcba9da8b Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 22 Sep 2026 12:14:31 +0800 Subject: [PATCH 63/85] cnat: measure the scan budget from inside the barrier (#18) cnat_session_scan holds the worker barrier for the whole scan and stops examining buckets once it has spent 100us, but it measured those 100us from the start time the scanner process took before it called into the scan, and that call is preceded by the barrier sync and by cnat_client_throttle_pool_process(). On a busy system the wait for the barrier alone exceeds the budget, so every call left the loop before its first bucket, returned the same starting point to the caller and paid the barrier cost again: no session was examined and stale sessions were never collected. Start the budget once the barrier is held and the buckets are about to be walked, which is the interval the budget is meant to bound. The check then passes on the first bucket of every call, so the scan always advances. Type: fix Change-Id: I0000000000000000000000000000000000000003 Signed-off-by: Bo Xu --- src/plugins/cnat/cnat_session.c | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/plugins/cnat/cnat_session.c b/src/plugins/cnat/cnat_session.c index aa4bbb32fab5..8cc8c9d948f6 100644 --- a/src/plugins/cnat/cnat_session.c +++ b/src/plugins/cnat/cnat_session.c @@ -501,6 +501,7 @@ cnat_session_scan (vlib_main_t *vm, f64 start_time, int i) { cnat_bihash_kv_t *stale = NULL, *key; BVT (clib_bihash) * h = &cnat_session_db; + f64 budget_start; int j, k; cnat_log_scanner_start (i); @@ -529,10 +530,20 @@ cnat_session_scan (vlib_main_t *vm, f64 start_time, int i) if (!h->instantiated) goto remove_stale; + /* + * The budget below is how long this scan may keep the workers off between + * two pauses, so it has to start here rather than at the caller's start + * time, which also counts the wait for the barrier. On a busy system that + * wait exceeds the budget on its own, the loop would then leave before its + * first bucket, hand the same starting point back to the caller and never + * examine a session. + */ + budget_start = vlib_time_now (vm); + for ( /* caller saves starting point */ ; i < h->nbuckets; i++) { /* allow no more than 100us without a pause */ - if ((vlib_time_now (vm) - start_time) > 10e-5) + if ((vlib_time_now (vm) - budget_start) > 10e-5) goto remove_stale; if (i < (h->nbuckets - 3)) From 64ada0a3181ae280626fbcd1b9a353835179dbe8 Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 22 Sep 2026 13:30:35 +0800 Subject: [PATCH 64/85] ipip: bound the tunnel lookups and fix the teib index checks (#19) * cnat: measure the scan budget from inside the barrier cnat_session_scan holds the worker barrier for the whole scan and stops examining buckets once it has spent 100us, but it measured those 100us from the start time the scanner process took before it called into the scan, and that call is preceded by the barrier sync and by cnat_client_throttle_pool_process(). On a busy system the wait for the barrier alone exceeds the budget, so every call left the loop before its first bucket, returned the same starting point to the caller and paid the barrier cost again: no session was examined and stale sessions were never collected. Start the budget once the barrier is held and the buckets are about to be walked, which is the interval the budget is meant to bound. The check then passes on the first bucket of every call, so the scan always advances. Type: fix Change-Id: I0000000000000000000000000000000000000003 Signed-off-by: Bo Xu * ipip: bound the tunnel lookups and fix the teib index checks mipip_update_adj indexed the sw_if_index to tunnel mapping with the interface it was handed and then took the tunnel from whatever index it read. The adjacency code resolves the device class from the super interface of the interface it is given and passes that interface's own sw_if_index, so a sub-interface of a p2mp tunnel indexes the mapping past its end: the read returns what follows the vector, the pool index is not the sentinel, and the tunnel fixup is taken from another tunnel's slot or from unallocated memory. A route via such a sub-interface reaches this. The teib callbacks checked the index against the length of the mapping with a strict less-than, so an index equal to the length still read one element past the end. A teib entry on any interface created after the last tunnel has such an index; the entry added callback then registered a tunnel key built from that read, which is how a loopback teib entry produced a spurious tunnel in the ipip tunnel database. This is the same off-by-one that gre fixed, left unfixed here. mipip_update_adj now looks the tunnel up through the helper the rest of the file uses, and the helper also rejects a pool index that is free. The two teib callbacks compare with less-than-or-equal. Type: fix Change-Id: I0000000000000000000000000000000000000004 Signed-off-by: Bo Xu --------- Signed-off-by: Bo Xu --- src/plugins/ipip/ipip.c | 17 ++++--- test/test_ipip.py | 110 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+), 8 deletions(-) diff --git a/src/plugins/ipip/ipip.c b/src/plugins/ipip/ipip.c index 10e231979325..ed8f2a08935c 100644 --- a/src/plugins/ipip/ipip.c +++ b/src/plugins/ipip/ipip.c @@ -440,18 +440,17 @@ mipip_mk_incomplete_walk (adj_index_t ai, void *data) void mipip_update_adj (vnet_main_t * vnm, u32 sw_if_index, adj_index_t ai) { - ipip_main_t *gm = &ipip_main; adj_midchain_fixup_t fixup; ip_adjacency_t *adj; teib_entry_t *ne; ipip_tunnel_t *t; adj_flags_t af; - u32 ti; af = ADJ_FLAG_NONE; adj = adj_get (ai); - ti = gm->tunnel_index_by_sw_if_index[sw_if_index]; - t = pool_elt_at_index (gm->tunnels, ti); + t = ipip_tunnel_db_find_by_sw_if_index (sw_if_index); + if (!t) + return; ne = teib_entry_find_46 (sw_if_index, adj->ia_nh_proto, &adj->sub_type.nbr.next_hop); @@ -584,10 +583,12 @@ ipip_tunnel_t * ipip_tunnel_db_find_by_sw_if_index (u32 sw_if_index) { ipip_main_t *gm = &ipip_main; + u32 ti; + if (vec_len (gm->tunnel_index_by_sw_if_index) <= sw_if_index) return NULL; - u32 ti = gm->tunnel_index_by_sw_if_index[sw_if_index]; - if (ti == ~0) + ti = gm->tunnel_index_by_sw_if_index[sw_if_index]; + if (ti == ~0 || pool_is_free_index (gm->tunnels, ti)) return NULL; return pool_elt_at_index (gm->tunnels, ti); } @@ -655,7 +656,7 @@ ipip_teib_entry_added (const teib_entry_t * ne) u32 t_idx; sw_if_index = teib_entry_get_sw_if_index (ne); - if (vec_len (gm->tunnel_index_by_sw_if_index) < sw_if_index) + if (vec_len (gm->tunnel_index_by_sw_if_index) <= sw_if_index) return; t_idx = gm->tunnel_index_by_sw_if_index[sw_if_index]; @@ -692,7 +693,7 @@ ipip_teib_entry_deleted (const teib_entry_t * ne) u32 t_idx; sw_if_index = teib_entry_get_sw_if_index (ne); - if (vec_len (gm->tunnel_index_by_sw_if_index) < sw_if_index) + if (vec_len (gm->tunnel_index_by_sw_if_index) <= sw_if_index) return; t_idx = gm->tunnel_index_by_sw_if_index[sw_if_index]; diff --git a/test/test_ipip.py b/test/test_ipip.py index b92f7c54d07f..288330fa8f19 100644 --- a/test/test_ipip.py +++ b/test/test_ipip.py @@ -689,6 +689,116 @@ def test_mipip4(self): ipip_if.unconfig_ip4() ipip_if.set_table_ip4(0) + def test_teib_on_interface_past_tunnel_mapping(self): + """a TEIB entry on an interface past the tunnel mapping is ignored""" + + # + # the mapping from sw_if_index to tunnel grows to cover every index a + # tunnel ever took, so the index equal to its length is the one with no + # slot at all. The loopbacks created first take every free interface + # index, which leaves the tunnel created next at the top of the + # interface pool, and the loopback created after it takes exactly the + # index one past the end of the mapping. The TEIB entry added on that + # loopback reaches the ipip teib callback with such an index. An + # index further past the end is rejected before the read, so the index + # has to be this one. + # + loops = self.create_loopback_interfaces(32) + + tun = VppIpIpTunInterface( + self, + self.pg0, + self.pg0.local_ip4, + "0.0.0.0", + mode=(VppEnum.vl_api_tunnel_mode_t.TUNNEL_API_MODE_MP), + ) + tun.add_vpp_config() + self.logger.info("ipip tunnel sw_if_index=%d" % tun.sw_if_index) + + lo = self.create_loopback_interfaces(1)[0] + loops.append(lo) + + # + # the loopback created before the tunnel and the tunnel itself have to + # be consecutive, otherwise a free interface index survived the + # loopbacks, the tunnel did not take the top of the pool, and the + # mapping can already reach past the index of the loopback created + # after it, which would leave the entry below short of the read. + # + self.assertEqual(loops[-2].sw_if_index + 1, tun.sw_if_index) + self.assertEqual(lo.sw_if_index, tun.sw_if_index + 1) + + teib = VppTeib(self, lo, "10.0.0.1", self.pg0.remote_ip4) + teib.add_vpp_config() + self.assertTrue(teib.query_vpp_config()) + + # + # the loopback is not a tunnel, so the tunnel database must still + # hold the one tunnel; before the bound was fixed the lookup ran off + # the end of the mapping and registered a key built from whatever + # followed it. + # + self.assertEqual( + len(self.vapi.cli("show ipip tunnel-hash").strip().splitlines()), 1 + ) + + teib.remove_vpp_config() + for loop in loops: + loop.remove_vpp_config() + tun.remove_vpp_config() + + def test_route_via_mipip_subif(self): + """a route via a sub-interface on a stale tunnel mapping slot""" + + # + # ipip deletes a tunnel by putting the invalid sentinel in its slot + # of the sw_if_index to tunnel mapping and leaving the mapping at its + # length, so the next interface to be created reuses the sentinel's + # sw_if_index. The adjacency of a p2mp tunnel is built through the + # tunnel's update_adjacency callback, which is handed that interface's + # own index; unguarded it dereferenced the sentinel as a pool index. + # + t1 = VppIpIpTunInterface( + self, + self.pg0, + self.pg0.local_ip4, + "0.0.0.0", + mode=(VppEnum.vl_api_tunnel_mode_t.TUNNEL_API_MODE_MP), + ) + t1.add_vpp_config() + t1_index = t1.sw_if_index + + t2 = VppIpIpTunInterface( + self, + self.pg0, + "10.9.9.9", + "0.0.0.0", + mode=(VppEnum.vl_api_tunnel_mode_t.TUNNEL_API_MODE_MP), + ) + t2.add_vpp_config() + t2.admin_up() + + t1.remove_vpp_config() + + r = self.vapi.create_vlan_subif(t2.sw_if_index, 100) + sub_index = r.sw_if_index + self.assertEqual(sub_index, t1_index) + + route = VppIpRoute( + self, "10.99.0.0", 24, [VppRoutePath("192.0.2.1", sub_index)] + ) + route.add_vpp_config() + + # + # the interface is not a tunnel, so the next hop stays unresolved and + # the route must be installed as a drop rather than taking the tunnel + # from the slot the index pointed at. + # + self.assertIn("dpo-drop ip4", self.vapi.cli("show ip fib 10.99.0.0/24")) + + route.remove_vpp_config() + t2.remove_vpp_config() + @unittest.skipIf("ipip" in config.excluded_plugins, "Exclude IPIP plugin tests") class TestIPIP6(VppTestCase): From f6ebc01bca9830f640196d3611d2dc9ab41f68c1 Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 22 Sep 2026 14:40:03 +0800 Subject: [PATCH 65/85] pppoe: bound the session lookup by sw_if_index (#20) pppoe_build_rewrite and pppoe_update_adj indexed the sw_if_index to session mapping with the interface they were handed and used the value they read as a session index. The adjacency code resolves the class of an interface to the class of its super interface but passes the interface's own sw_if_index, so a VLAN sub-interface created on a PPPoE session indexes the mapping exactly one past its end. That read returns whatever the vector holds past its length: in the observed run it returned 0, so a route through the sub-interface took the PPPoE rewrite of the first session, and a packet sent over the route was encapsulated for that session rather than passed to the ARP resolution of the sub-interface. A value that names another session, a free pool slot or an index past the end of the pool is used as a session pointer the same way. Both call sites now look the session up through one helper that bounds the mapping, rejects the invalid sentinel and rejects a free pool index. pppoe_build_rewrite returns no rewrite when the lookup fails, as gre_build_rewrite does, so the adjacency keeps the ARP resolution of the sub-interface, and pppoe_update_adj returns leaving the adjacency as it was allocated. Type: fix Change-Id: I0000000000000000000000000000000000000005 Signed-off-by: Bo Xu --- src/plugins/pppoe/pppoe.c | 37 +++++++++++---- test/test_pppoe.py | 99 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+), 8 deletions(-) diff --git a/src/plugins/pppoe/pppoe.c b/src/plugins/pppoe/pppoe.c index 0f5c395ecc46..20c244d839cb 100644 --- a/src/plugins/pppoe/pppoe.c +++ b/src/plugins/pppoe/pppoe.c @@ -79,21 +79,43 @@ format_pppoe_header_with_length (u8 * s, va_list * args) return s; } +/* + * Find the session that owns a sw_if_index. The mapping is indexed with + * the interface the caller was handed, which for a sub-interface is the + * sub-interface's own index, so it can be one past the end of the mapping + * and the slot it names can be free or hold the invalid sentinel. + */ +static pppoe_session_t * +pppoe_session_find_by_sw_if_index (u32 sw_if_index) +{ + pppoe_main_t *pem = &pppoe_main; + u32 session_id; + + if (vec_len (pem->session_index_by_sw_if_index) <= sw_if_index) + return NULL; + + session_id = pem->session_index_by_sw_if_index[sw_if_index]; + + if (session_id == ~0 || pool_is_free_index (pem->sessions, session_id)) + return NULL; + + return pool_elt_at_index (pem->sessions, session_id); +} + static u8 * pppoe_build_rewrite (vnet_main_t * vnm, u32 sw_if_index, vnet_link_t link_type, const void *dst_address) { - pppoe_main_t *pem = &pppoe_main; pppoe_session_t *t; vnet_hw_interface_t *hi; vnet_sw_interface_t *si; pppoe_header_t *pppoe; - u32 session_id; u8 *rw = 0; - session_id = pem->session_index_by_sw_if_index[sw_if_index]; - t = pool_elt_at_index (pem->sessions, session_id); + t = pppoe_session_find_by_sw_if_index (sw_if_index); + if (!t) + return 0; int len = sizeof (pppoe_header_t) + sizeof (ethernet_header_t); si = vnet_get_sw_interface (vnm, t->encap_if_index); @@ -173,18 +195,17 @@ pppoe_fixup (vlib_main_t * vm, static void pppoe_update_adj (vnet_main_t * vnm, u32 sw_if_index, adj_index_t ai) { - pppoe_main_t *pem = &pppoe_main; dpo_id_t dpo = DPO_INVALID; ip_adjacency_t *adj; pppoe_session_t *t; vnet_sw_interface_t *si; - u32 session_id; ASSERT (ADJ_INDEX_INVALID != ai); adj = adj_get (ai); - session_id = pem->session_index_by_sw_if_index[sw_if_index]; - t = pool_elt_at_index (pem->sessions, session_id); + t = pppoe_session_find_by_sw_if_index (sw_if_index); + if (!t) + return; uword len = sizeof (ethernet_header_t); diff --git a/test/test_pppoe.py b/test/test_pppoe.py index b0ea5cfe4f4e..1c0f959d9df3 100644 --- a/test/test_pppoe.py +++ b/test/test_pppoe.py @@ -645,6 +645,105 @@ def test_PPPoE_Encap_Multiple(self): # Delete a route that resolves the server's destination route_sever_dst.remove_vpp_config() + def test_route_via_pppoe_subif(self): + """a route via a sub-interface created on a PPPoE session""" + + # + # the mapping from sw_if_index to session grows to cover the index of + # every session, so the index equal to its length is the one with no + # slot at all. The loopbacks take every free interface index left by + # the earlier test cases, the sessions below take the indices those + # cases released until one of them is given a new index above the + # loopbacks, and the sub-interface created on that session takes + # exactly the index one past the end of the mapping. The adjacency + # code resolves the interface class of a sub-interface to the class of + # its super interface but hands the callbacks the sub-interface's own + # index, so the route below reaches them with that index. + # + loops = self.create_loopback_interfaces(32) + + # Send PPPoE Discovery + tx0 = self.create_stream_pppoe_discovery( + self.pg0, self.pg1, self.pg0.remote_mac + ) + self.pg0.add_stream(tx0) + self.pg_start() + + # + # a session deleted by an earlier test case leaves its interface index + # on the free list, and the next session takes that index back instead + # of a new one, so keep adding sessions until one lands past the last + # loopback and the session below is at the top of the interface pool. + # + sessions = [] + for client_session_id in range(self.session_id, self.session_id + 64): + session = VppPppoeInterface( + self, self.pg0.remote_ip4, self.pg0.remote_mac, client_session_id + ) + session.add_vpp_config() + sessions.append(session) + if session.sw_if_index > loops[-1].sw_if_index: + break + pppoe_if = sessions[-1] + self.logger.info("pppoe session sw_if_index=%d" % pppoe_if.sw_if_index) + + # + # the session has to be the interface right after the last loopback, + # otherwise the mapping can already reach past its index and the + # sub-interface below never leaves the end of it. + # + self.assertEqual(loops[-1].sw_if_index + 1, pppoe_if.sw_if_index) + + r = self.vapi.create_vlan_subif(pppoe_if.sw_if_index, 100) + sub_index = r.sw_if_index + self.logger.info("pppoe session sub-interface sw_if_index=%d" % sub_index) + self.assertEqual(sub_index, pppoe_if.sw_if_index + 1) + + # + # the sub-interface is created down, and a route through a down + # interface is a drop whatever the session lookup returns, so the + # interface has to be up for the lookup to decide the outcome. + # + self.vapi.sw_interface_set_flags(sub_index, 1) + + route = VppIpRoute( + self, "10.99.0.0", 24, [VppRoutePath("192.0.2.1", sub_index)] + ) + route.add_vpp_config() + + # + # the sub-interface is not a session, so its adjacency must not be + # given the PPPoE rewrite of one. With the lookup unbounded the + # callback read the slot one past the end of the mapping, then installed + # that session's rewrite on this route. + # + fib = self.vapi.cli("show ip fib 10.99.0.0/24") + self.logger.info(fib) + self.assertIn("10.99.0.0/24 fib:0", fib) + self.assertIn("oper-flags:resolved", fib) + self.assertIn("arp-ipv4: via 0.0.0.0", fib) + self.assertNotIn("stacked-on:", fib) + + # + # a packet routed through the sub-interface must not leave encapsulated + # for a session either, and the trace shows it stopping in the ARP node. + # + self.vapi.cli("clear trace") + self.pg1.add_stream( + self.create_stream_ip4(self.pg1, self.pg0, "10.99.0.1", "10.99.0.2") + ) + self.pg_start() + trace = self.vapi.cli("show trace") + self.logger.info(trace) + self.assertIn("ip4-arp", trace) + + route.remove_vpp_config() + self.vapi.delete_subif(sub_index) + for session in reversed(sessions): + session.remove_vpp_config() + for loop in loops: + loop.remove_vpp_config() + if __name__ == "__main__": unittest.main(testRunner=VppTestRunner) From 420e5a1180a829b8a457f7e28ffacdb04f277a17 Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 22 Sep 2026 15:26:44 +0800 Subject: [PATCH 66/85] cnat: skip path allocation for an empty path list (#21) cnat_translation_update carries paths[n_paths], and n_paths is a caller-supplied count. A zero count is well formed: the generated cnat_translation_calc_size() adds clib_net_to_host_u32(n_paths) * sizeof(paths[0]) to the fixed base size, so a message with n_paths == 0 is exactly the base size and vlibapi accepts and dispatches it (see api_shared.c msg_len vs calc_size check). The handler then computed n_paths - 1 in u32, wrapping to 0xFFFFFFFF, and asked vec_validate() to size 2^32 elements. That is a request of hundreds of gigabytes, and the main heap is created with mspace_disable_expand(). On an unpatched build the resulting mspace_memalign() returns NULL and clib_mem_heap_alloc_aligned() calls os_out_of_memory() because it passes os_out_of_memory_on_failure = 1, which prints "Out-of-memory, calling os_panic()." and panics the process. A single ordinary API call from any client reaches it; no packet is needed. An empty path list is a state the rest of the plugin already expects: cnat_translation_update() guards its own n_paths - 1 allocation with "if (n_paths)" (cnat_translation.c:325), cnat_translation_stack() builds a load balance with zero buckets and only initialises maglev when paths exist, cnat_load_balance() short-circuits on a zero-bucket load balance, cnat_translation_init_maglev() returns early on an empty active path list, and the translation_details reply already sizes itself from vec_len(ct->ct_paths_indexes). So keep the zero path list valid and skip the allocation instead of rejecting the message: guard the vec_validate() with the same "if (n_paths)" shape used at cnat_translation.c:325. paths stays NULL, vec_len() then reports zero and vec_free() is a no-op. This is the only count-derived "- 1" allocation in the plugin reachable from a message: the remaining sites in cnat_maglev.c are guarded or use internal quantities (maglev_len, num_backends, n_tests, n_remove, n_changes) that are never zero when reached. Type: fix Change-Id: I0000000000000000000000000000000000000006 Signed-off-by: Bo Xu --- src/plugins/cnat/cnat_api.c | 5 ++++- test/test_cnat.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/plugins/cnat/cnat_api.c b/src/plugins/cnat/cnat_api.c index 7806e9de0f97..f853185240db 100644 --- a/src/plugins/cnat/cnat_api.c +++ b/src/plugins/cnat/cnat_api.c @@ -156,7 +156,10 @@ vl_api_cnat_translation_update_t_handler (vl_api_cnat_translation_update_t goto done; n_paths = clib_net_to_host_u32 (mp->translation.n_paths); - vec_validate (paths, n_paths - 1); + /* An empty path list is a valid message: n_paths - 1 would wrap to + * ~0 and request an absurd allocation. */ + if (n_paths) + vec_validate (paths, n_paths - 1); for (pi = 0; pi < n_paths; pi++) { diff --git a/test/test_cnat.py b/test/test_cnat.py index 414d8e451c9d..ad576a974e68 100644 --- a/test/test_cnat.py +++ b/test/test_cnat.py @@ -1036,6 +1036,36 @@ def test_cnat_fhc(self): self._make_multi_backend_translations() self.cnat_fhc_translation() + def test_cnat_zero_paths(self): + # """ CNat Translation with an empty path list """ + self.translations = [] + self.mbtranslations = [] + translation = Translation( + self, + TCP, + Endpoint(ip="30.0.0.8", port=8888, is_v6=False), + [], + 0x9F, + ).add_vpp_config() + self.translations.append(translation) + try: + self.assertIsNotNone(translation.id) + dump = translation.query_vpp_config() + self.assertIsNotNone(dump) + self.assertEqual(0, dump.n_paths) + translation_lines = [ + line + for line in self.vapi.cli("show cnat translation").splitlines() + if line.startswith(f"[{translation.id}]") + ] + self.logger.info(translation_lines) + self.assertEqual(1, len(translation_lines)) + self.assertIn("30.0.0.8;8888 TCP", translation_lines[0]) + self.assertIn("lb:default", translation_lines[0]) + finally: + self.translations.remove(translation) + translation.remove_vpp_config() + @unittest.skipIf("cnat" in config.excluded_plugins, "Exclude CNAT plugin tests") class TestCNatTranslationCLI(CnatCommonTestCase): From 77f63c0e8d530a9745f45cc710662448a30e9dad Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 22 Sep 2026 16:24:28 +0800 Subject: [PATCH 67/85] papi: read the public version attribute of an IP address (#22) add_convenience_methods() installs the vapi_af and vapi_af_name properties on ipaddress._IPAddressBase, and both read self._version. That attribute is private to the ipaddress module and Python 3.14 removed it, so on that version every call raised "AttributeError: 'IPv4Address' object has no attribute '_version'" and every code path that reached the properties broke. The test framework reads .vapi_af while it resolves neighbours, so whole test classes failed even though nothing about the address itself was wrong. On Python 3.13, which the CI container runs, the same code still worked, so the defect went unnoticed. ipaddress addresses expose the public version attribute, which the module has kept across all versions and which returns 4 or 6 for an address. Read that instead, in both vpp_papi.py and vpp_papi_async.py, and keep the ValueError for any other value; its message no longer names the private attribute. These eight reads were the only use of a private ipaddress attribute in src/vpp-api/python and test: nothing there reads _max_prefixlen, _string_from_ip_int, _parse_octet, _split_optional_netmask, _make_netmask or _check_int_address. The properties cannot return anything until the address_family enum is in the type table, so the new test in vpp_papi/tests/test_vapi_af.py feeds a small enum JSON to VPPApiJSONFiles and then checks both properties on an IPv4 and an IPv6 address for both modules. Type: fix Change-Id: I0000000000000000000000000000000000000007 Signed-off-by: Bo Xu --- .../python/vpp_papi/tests/test_vapi_af.py | 66 +++++++++++++++++++ src/vpp-api/python/vpp_papi/vpp_papi.py | 12 ++-- src/vpp-api/python/vpp_papi/vpp_papi_async.py | 12 ++-- 3 files changed, 78 insertions(+), 12 deletions(-) create mode 100644 src/vpp-api/python/vpp_papi/tests/test_vapi_af.py diff --git a/src/vpp-api/python/vpp_papi/tests/test_vapi_af.py b/src/vpp-api/python/vpp_papi/tests/test_vapi_af.py new file mode 100644 index 000000000000..e9d41ed1c221 --- /dev/null +++ b/src/vpp-api/python/vpp_papi/tests/test_vapi_af.py @@ -0,0 +1,66 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2026 Bo Xu + +import ipaddress +import unittest + +from vpp_papi import vpp_papi +from vpp_papi import vpp_papi_async + +JSON_API = """\ +{ + "enums": [ + [ + "address_family", + [ + "ADDRESS_IP4", + 0 + ], + [ + "ADDRESS_IP6", + 1 + ], + { + "enumtype": "u8" + } + ] + ] +} +""" + + +class TestVapiAf(unittest.TestCase): + """The vapi_af and vapi_af_name properties of an IP address.""" + + def setUp(self): + # The properties resolve the address_family enum through the type + # table, so make sure that enum is known. + vpp_papi.VPPApiJSONFiles().process_json_str(JSON_API) + + def test_vapi_af_and_name_of_ip_address(self): + af = vpp_papi.VppEnum.vl_api_address_family_t + + # Both modules install the properties on the same base class, so + # install one at a time to exercise both implementations. + for module in (vpp_papi, vpp_papi_async): + with self.subTest(module=module.__name__): + module.add_convenience_methods() + ip4 = ipaddress.ip_address("1.2.3.4") + ip6 = ipaddress.ip_address("::1") + self.assertEqual(af.ADDRESS_IP4.value, ip4.vapi_af) + self.assertEqual(af.ADDRESS_IP6.value, ip6.vapi_af) + self.assertEqual("ip4", ip4.vapi_af_name) + self.assertEqual("ip6", ip6.vapi_af_name) + + def test_vapi_af_rejects_other_versions(self): + class NotAnIpAddress(ipaddress.IPv4Address): + version = 5 + + for module in (vpp_papi, vpp_papi_async): + with self.subTest(module=module.__name__): + module.add_convenience_methods() + addr = NotAnIpAddress("1.2.3.4") + for attr in ("vapi_af", "vapi_af_name"): + with self.assertRaises(ValueError) as cm: + getattr(addr, attr) + self.assertIn("Invalid version", str(cm.exception)) diff --git a/src/vpp-api/python/vpp_papi/vpp_papi.py b/src/vpp-api/python/vpp_papi/vpp_papi.py index 53e830ea4eb4..a7bac294d78f 100644 --- a/src/vpp-api/python/vpp_papi/vpp_papi.py +++ b/src/vpp-api/python/vpp_papi/vpp_papi.py @@ -99,18 +99,18 @@ def vpp_atexit(vpp_weakref): def add_convenience_methods(): # provide convenience methods to IP[46]Address.vapi_af def _vapi_af(self): - if 6 == self._version: + if 6 == self.version: return VppEnum.vl_api_address_family_t.ADDRESS_IP6.value - if 4 == self._version: + if 4 == self.version: return VppEnum.vl_api_address_family_t.ADDRESS_IP4.value - raise ValueError("Invalid _version.") + raise ValueError("Invalid version.") def _vapi_af_name(self): - if 6 == self._version: + if 6 == self.version: return "ip6" - if 4 == self._version: + if 4 == self.version: return "ip4" - raise ValueError("Invalid _version.") + raise ValueError("Invalid version.") ipaddress._IPAddressBase.vapi_af = property(_vapi_af) ipaddress._IPAddressBase.vapi_af_name = property(_vapi_af_name) diff --git a/src/vpp-api/python/vpp_papi/vpp_papi_async.py b/src/vpp-api/python/vpp_papi/vpp_papi_async.py index c8a51896a7ce..3ae73ddfc043 100644 --- a/src/vpp-api/python/vpp_papi/vpp_papi_async.py +++ b/src/vpp-api/python/vpp_papi/vpp_papi_async.py @@ -88,18 +88,18 @@ def vpp_atexit(vpp_weakref): def add_convenience_methods(): # provide convenience methods to IP[46]Address.vapi_af def _vapi_af(self): - if 6 == self._version: + if 6 == self.version: return VppEnum.vl_api_address_family_t.ADDRESS_IP6.value - if 4 == self._version: + if 4 == self.version: return VppEnum.vl_api_address_family_t.ADDRESS_IP4.value - raise ValueError("Invalid _version.") + raise ValueError("Invalid version.") def _vapi_af_name(self): - if 6 == self._version: + if 6 == self.version: return "ip6" - if 4 == self._version: + if 4 == self.version: return "ip4" - raise ValueError("Invalid _version.") + raise ValueError("Invalid version.") ipaddress._IPAddressBase.vapi_af = property(_vapi_af) ipaddress._IPAddressBase.vapi_af_name = property(_vapi_af_name) From 42eb170c0fb7bcd739e71a94e79183d38ddcc7ae Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 22 Sep 2026 17:05:29 +0800 Subject: [PATCH 68/85] ipip: check the tunnel mode in the 6RD delete path (#23) sixrd_del_tunnel() looks the tunnel up by sw_if_index and never checks that the tunnel was created by sixrd_add_tunnel(). For a plain IPIP tunnel the nested sixrd struct is zeroed, so ip6_fib_index is 0 and the call removes a path from the default IPv6 table and then unlocks FIB_SOURCE_6RD on it. fib_table_lock() with FIB_SOURCE_6RD is only ever called by sixrd_add_tunnel(), so fib_table_unlock() reaches the ASSERT (ft_locks[FIB_SOURCE_6RD] > 0) in fib_table_lock_dec(), which aborts a debug build and underflows the lock counter in a release build. The API then reports success and the plain tunnel is deleted. Reject a tunnel that is not in IPIP_MODE_6RD with VNET_API_ERROR_INVALID_VALUE and leave it untouched. Type: fix Change-Id: I0000000000000000000000000000000000000009 Signed-off-by: Bo Xu --- src/plugins/ipip/sixrd.c | 7 +++++ test/test_ipip.py | 63 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/src/plugins/ipip/sixrd.c b/src/plugins/ipip/sixrd.c index 646c0dd3f0bb..a0fa49cad4c9 100644 --- a/src/plugins/ipip/sixrd.c +++ b/src/plugins/ipip/sixrd.c @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -371,6 +372,12 @@ sixrd_del_tunnel (u32 sw_if_index) return -1; } + if (IPIP_MODE_6RD != t->mode) + { + clib_warning ("SIXRD tunnel delete: not a 6RD tunnel: %d", sw_if_index); + return VNET_API_ERROR_INVALID_VALUE; + } + fib_prefix_t pfx6 = { .fp_proto = FIB_PROTOCOL_IP6, .fp_len = t->sixrd.ip6_prefix_len, diff --git a/test/test_ipip.py b/test/test_ipip.py index 288330fa8f19..375f99abf326 100644 --- a/test/test_ipip.py +++ b/test/test_ipip.py @@ -29,14 +29,14 @@ """ -def ipip_add_tunnel(test, src, dst, table_id=0, dscp=0x0, flags=0): +def ipip_add_tunnel(test, src, dst, table_id=0, dscp=0x0, flags=0, instance=0xFFFFFFFF): """Add a IPIP tunnel""" return test.vapi.ipip_add_tunnel( tunnel={ "src": src, "dst": dst, "table_id": table_id, - "instance": 0xFFFFFFFF, + "instance": instance, "dscp": dscp, "flags": flags, } @@ -1599,5 +1599,64 @@ def test_mpls(self): tun6.disable_mpls() +@unittest.skipIf("ipip" in config.excluded_plugins, "Exclude IPIP plugin tests") +class TestIPIP6rdTunnelDelete(VppTestCase): + """6RD tunnel delete test case""" + + # src/vnet/error.h: VNET_API_ERROR_INVALID_VALUE + VNET_API_ERROR_INVALID_VALUE = -7 + + def setUp(self): + super(TestIPIP6rdTunnelDelete, self).setUp() + self.ip4_table = VppIpTable(self, 4).add_vpp_config() + self.ip6_table = VppIpTable(self, 6, True).add_vpp_config() + + def test_sixrd_del_tunnel_rejects_plain_ipip_tunnel(self): + """delete a plain IPIP tunnel through the 6RD entry point""" + tunnel = ipip_add_tunnel(self, "1.1.1.1", "2.2.2.2", instance=100) + sw_if_index = tunnel.sw_if_index + + with self.vapi.assert_known_api_retval( + retvals=[self.VNET_API_ERROR_INVALID_VALUE] + ): + self.vapi.ipip_6rd_del_tunnel(sw_if_index=sw_if_index) + + self.assertFalse(self.vpp_dead) + tunnels = self.vapi.ipip_tunnel_dump(sw_if_index=sw_if_index) + self.assertEqual(1, len(tunnels)) + self.assertEqual(sw_if_index, tunnels[0].tunnel.sw_if_index) + + self.vapi.ipip_del_tunnel(sw_if_index=sw_if_index) + + def test_sixrd_del_tunnel_cli_rejects_plain_ipip_tunnel(self): + """delete a plain IPIP tunnel through the 6RD CLI entry point""" + tunnel = ipip_add_tunnel(self, "3.3.3.3", "4.4.4.4", instance=101) + sw_if_index = tunnel.sw_if_index + + self.vapi.cli("delete 6rd tunnel sw_if_index %d" % sw_if_index) + + self.assertFalse(self.vpp_dead) + tunnels = self.vapi.ipip_tunnel_dump(sw_if_index=sw_if_index) + self.assertEqual(1, len(tunnels)) + + self.vapi.ipip_del_tunnel(sw_if_index=sw_if_index) + + def test_sixrd_del_tunnel_removes_6rd_tunnel(self): + """delete a 6RD tunnel through the 6RD entry point""" + tunnel = self.vapi.ipip_6rd_add_tunnel( + ip6_table_id=self.ip6_table.table_id, + ip4_table_id=self.ip4_table.table_id, + ip6_prefix="2002::/16", + ip4_prefix="0.0.0.0/0", + ip4_src="10.0.0.1", + security_check=True, + ) + sw_if_index = tunnel.sw_if_index + + self.vapi.ipip_6rd_del_tunnel(sw_if_index=sw_if_index) + + self.assertEqual([], self.vapi.ipip_tunnel_dump(sw_if_index=sw_if_index)) + + if __name__ == "__main__": unittest.main(testRunner=VppTestRunner) From de8a6f04759f0098417851025c398ca2a404dc77 Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 22 Sep 2026 17:30:42 +0800 Subject: [PATCH 69/85] cnat: bound the snat policy fib index (#24) cnat_set_snat_addresses_v2 carries fwd_fib_index and ret_fib_index, and the handler passes both of them to cnat_set_snat() without looking at them (cnat_api.c:415). The plugin keeps its per-VRF state in vectors indexed by the fib index a packet arrives in, so cnat_set_snat() hands fwd_fib_index to the source port allocator (:685), to the vec_validate() calls of the ip4 policy and session vectors (:697, :700) and to their ip6 counterparts (:731, :734). A well formed message with fwd_fib_index = ~0 asks the main heap for 2^32 entries; the heap is created with mspace_disable_expand(), mspace_memalign() returns NULL and clib_mem_heap_alloc_aligned() calls os_out_of_memory() with os_out_of_memory_on_failure set, which panics vpp. The CLI reaches the same allocation: "set cnat snat-policy addr fib -1" is accepted because unformat %d stores a leading minus sign into the u32 index (unformat.c:520). ret_fib_index is dereferenced as well, through cnat_client_add_pfx() (:720, :754) and fib_table_entry_special_dpo_add() (cnat_client.c:207), which resolves the index with fib_table_get() and pool_elt_at_index() and never checks it. That path is taken when the SNAT address is not an interface address yet, that is when cnat_resolve_addr() found no address of the family on the interface (cnat_types.c:87), which is the configuration test_cnat.py:1125 sets up, and from the address resolution callback (:523). Guard both indices at the entry of cnat_set_snat() so that one check covers the v1 handler, the v2 handler and the CLI: an index has to name a live table of the address family the policy direction uses, the condition fib_table_get_or_null() applies (fib_table.c:72), and a call that names no table returns VNET_API_ERROR_NO_SUCH_FIB (-3). The CLI reports that as "no such fib %d or rfib %d" instead of "unknown error -3". A policy that names an index which is live in one family only now returns -3 instead of being indexed into the vectors of the other family. A delete call names no direction and so is not covered by that check. The lookup it goes through answers with the default policy when the fib index names no policy of its own (cnat_snat_policy.h:132), and the update path then records the policy it found under that index (:699), so the slots of several fib indexes can name one entry and the entry a delete frees is not necessarily the entry the index names. Two changes keep that path inside the memory it owns. A delete has to name the entry it removes, that is the slot of the fib index has to hold the pool index of that entry, and a call that names no entry returns VNET_API_ERROR_NO_SUCH_FIB before it drops the watch of the policy the lookup answered with, so that a refused delete leaves that policy as it was; and freeing an entry empties every slot and the default pointer that name it instead of only the slots of the index the caller passed. Without the first change a delete for a fib index that names no policy frees the default policy and frees the port allocator of that index, which the index never initialised: cnat_free_port_allocator() read the element past the end of cnat_src_policy_main.src_ports and freed the bitmap and the lock it found there. Without the second one a delete for the index that created an entry leaves the slots of the other indexes that were configured for it pointing at the freed entry, and the next lookup of such an index reads a free pool slot in cnat_snat_policy_entry_get__(). The port allocator now bounds its index the way cnat_get_src_port_allocator() already does (cnat_src_policy.c:39). The indices a policy may name are table 0 of both families, which is created at startup (ip4_forward.c:1104, ip6_forward.c:2782), and the tables the user configured, so the configurations in the tree keep working. Type: fix Change-Id: I0000000000000000000000000000000000000008 Signed-off-by: Bo Xu --- src/plugins/cnat/cnat_snat_policy.c | 95 ++++++++++++++++++-- src/plugins/cnat/cnat_src_policy.c | 7 +- test/test_cnat.py | 135 ++++++++++++++++++++++++++++ 3 files changed, 231 insertions(+), 6 deletions(-) diff --git a/src/plugins/cnat/cnat_snat_policy.c b/src/plugins/cnat/cnat_snat_policy.c index 42949f2678b8..ae7bdf09c808 100644 --- a/src/plugins/cnat/cnat_snat_policy.c +++ b/src/plugins/cnat/cnat_snat_policy.c @@ -524,6 +524,48 @@ cnat_if_addr_add_del_snat_cb (addr_resolution_t *ar, ip_address_t *address, u8 i } } +/* + * The lookup of an index answers with the default policy when the index names + * no policy of its own, and the update path then records the policy it found + * under that index, so the slots of several indexes can name one entry. + * Freeing the entry has to empty all of them, and the default pointer too, or + * a later lookup follows a freed pool entry. + */ +static void +cnat_snat_policy_unregister (cnat_snat_policy_entry_t *cpe) +{ + cnat_snat_policy_main_t *cpm = &cnat_snat_policy_main; + u32 *slots[2] = { cpm->snat_policy_per_fwd_fib_index4, cpm->snat_policy_per_fwd_fib_index6 }; + u32 index = cpe - cpm->snat_policies_pool; + + for (int v = 0; v < ARRAY_LEN (slots); v++) + for (u32 i = 0; i < vec_len (slots[v]); i++) + if (index == vec_elt (slots[v], i)) + vec_elt (slots[v], i) = INDEX_INVALID; + + if (cpm->snat_default_policy == cpe) + cpm->snat_default_policy = 0; +} + +/* + * Whether the fib index names this entry, that is whether one of the slots of + * that index holds the pool index of the entry. Because of the fallback + * above, what a lookup answers with is not necessarily what the index names. + */ +static u8 +cnat_snat_policy_fib_names_entry (u32 index, u32 fwd_fib_index) +{ + cnat_snat_policy_main_t *cpm = &cnat_snat_policy_main; + + if (fwd_fib_index < vec_len (cpm->snat_policy_per_fwd_fib_index4) && + index == vec_elt (cpm->snat_policy_per_fwd_fib_index4, fwd_fib_index)) + return 1; + if (fwd_fib_index < vec_len (cpm->snat_policy_per_fwd_fib_index6) && + index == vec_elt (cpm->snat_policy_per_fwd_fib_index6, fwd_fib_index)) + return 1; + return 0; +} + static void cnat_snat_cleanup (cnat_snat_policy_main_t *cpm, cnat_snat_policy_entry_t *cpe, u32 fwd_fib_index) { @@ -539,10 +581,7 @@ cnat_snat_cleanup (cnat_snat_policy_main_t *cpm, cnat_snat_policy_entry_t *cpe, 0 /* is_session */); } - if (fwd_fib_index < vec_len (cpm->snat_policy_per_fwd_fib_index4)) - vec_elt (cpm->snat_policy_per_fwd_fib_index4, fwd_fib_index) = ~0; - if (fwd_fib_index < vec_len (cpm->snat_policy_per_fwd_fib_index6)) - vec_elt (cpm->snat_policy_per_fwd_fib_index6, fwd_fib_index) = ~0; + cnat_snat_policy_unregister (cpe); cnat_snat_policy_entry_cleanup (cpe); cnat_free_port_allocator (fwd_fib_index); @@ -550,6 +589,28 @@ cnat_snat_cleanup (cnat_snat_policy_main_t *cpm, cnat_snat_policy_entry_t *cpe, pool_put_index (cpm->snat_policies_pool, index); } +/* + * A fib index only means something if the FIB of that address family is + * there, which is the same condition fib_table_get_or_null() checks. Both + * indices of a policy are used as vector indices and, for the return + * direction, as an index into the FIB pool. + */ +static u8 +cnat_snat_fib_index_is_valid (u32 fib_index, fib_protocol_t proto) +{ + switch (proto) + { + case FIB_PROTOCOL_IP4: + return (fib_index < vec_len (ip4_main.fibs) && + !pool_is_free_index (ip4_main.fibs, fib_index)); + case FIB_PROTOCOL_IP6: + return (fib_index < vec_len (ip6_main.fibs) && + !pool_is_free_index (ip6_main.fibs, fib_index)); + default: + return 0; + } +} + __clib_export int cnat_set_snat (u32 fwd_fib_index, u32 ret_fib_index, const ip4_address_t *ip4, u8 ip4_pfx_len, const ip6_address_t *ip6, u8 ip6_pfx_len, u32 sw_if_index, @@ -567,6 +628,20 @@ cnat_set_snat (u32 fwd_fib_index, u32 ret_fib_index, const ip4_address_t *ip4, u if ((ip4_set && ip4_pfx_len > 32) || (ip6_set && (ip6_pfx_len < 64 || ip6_pfx_len > 128))) return VNET_API_ERROR_INVALID_VALUE; + /* + * The policy is kept in vectors indexed by the fwd fib index and the return + * path is installed in the ret fib index, both of which the caller (API or + * CLI) picked. Without this check an index such as ~0 makes the + * vec_validate() calls below reserve room for 2^32 entries. + */ + if ((sw_if_set || ip4_set) && (!cnat_snat_fib_index_is_valid (fwd_fib_index, FIB_PROTOCOL_IP4) || + !cnat_snat_fib_index_is_valid (ret_fib_index, FIB_PROTOCOL_IP4))) + return VNET_API_ERROR_NO_SUCH_FIB; + + if ((sw_if_set || ip6_set) && (!cnat_snat_fib_index_is_valid (fwd_fib_index, FIB_PROTOCOL_IP6) || + !cnat_snat_fib_index_is_valid (ret_fib_index, FIB_PROTOCOL_IP6))) + return VNET_API_ERROR_NO_SUCH_FIB; + cnat_lazy_init (); /* we can either: @@ -582,6 +657,13 @@ cnat_set_snat (u32 fwd_fib_index, u32 ret_fib_index, const ip4_address_t *ip4, u { /* entry found */ index = cpe - cpm->snat_policies_pool; + /* + * The lookup answers with the default policy when the fib index names + * no policy of its own; only the policy an index names may be deleted, + * and a refused delete has to leave the policy untouched. + */ + if (is_delete && !cnat_snat_policy_fib_names_entry (index, fwd_fib_index)) + return VNET_API_ERROR_NO_SUCH_FIB; /* if the interface is changed, unwatch it (note: also works for delete) */ if (cpe->snat_ip4.ce_sw_if_index != sw_if_index) @@ -723,7 +805,10 @@ cnat_set_snat_cli (vlib_main_t *vm, unformat_input_t *input, CNAT_SNAT_POLICY_FLAG_USE_AS_DEFAULT); if (rv) { - e = clib_error_return (0, "unknown error %d", rv); + if (VNET_API_ERROR_NO_SUCH_FIB == rv) + e = clib_error_return (0, "no such fib %d or rfib %d", fwd_fib_index, ret_fib_index); + else + e = clib_error_return (0, "unknown error %d", rv); goto done; } diff --git a/src/plugins/cnat/cnat_src_policy.c b/src/plugins/cnat/cnat_src_policy.c index c3d9cf266f7a..b83ad17f3383 100644 --- a/src/plugins/cnat/cnat_src_policy.c +++ b/src/plugins/cnat/cnat_src_policy.c @@ -120,7 +120,12 @@ void cnat_free_port_allocator (u32 fib_index) { cnat_src_policy_main_t *cspm = &cnat_src_policy_main; - cnat_src_port_allocator_t *src_ports = vec_elt (cspm->src_ports, fib_index); + cnat_src_port_allocator_t *src_ports; + + if (fib_index >= vec_len (cspm->src_ports)) + return; /* nothing was allocated for this fib */ + + src_ports = vec_elt (cspm->src_ports, fib_index); for (int i = 0; i < CNAT_N_SPORT_PROTO; i++) { clib_bitmap_free (src_ports[i].bmap); diff --git a/test/test_cnat.py b/test/test_cnat.py index ad576a974e68..de293e21815f 100644 --- a/test/test_cnat.py +++ b/test/test_cnat.py @@ -1110,6 +1110,9 @@ def test_cnat_cli_translation_validation(self): class TestCNatSourceNAT(CnatCommonTestCase): """CNat Source NAT""" + # vnet/error.h: no such FIB / VRF + VNET_API_ERROR_NO_SUCH_FIB = -3 + @classmethod def setUpClass(cls): super(TestCNatSourceNAT, cls).setUpClass() @@ -1432,6 +1435,138 @@ def test_snat_limit(self): self.vapi.cnat_session_purge() + def test_snat_invalid_fib_index(self): + """CNat Source Nat policy for a fib index that has no table""" + snat_ip4 = self.pg2.remote_hosts[0].ip4 + + # Both fib indices of the policy size the per-VRF state, so an index + # that names no table is refused instead of being allocated for. A + # request for ~0 entries used to abort vpp. + with self.vapi.assert_negative_api_retval(): + reply = self.vapi.cnat_set_snat_addresses_v2( + fwd_fib_index=0xFFFFFFFF, + ret_fib_index=0, + snat_ip4=snat_ip4, + snat_ip6=None, + sw_if_index=INVALID_INDEX, + flags=0, + ) + self.assertEqual(reply.retval, self.VNET_API_ERROR_NO_SUCH_FIB) + + # Same for the fib index the return path is installed in. + with self.vapi.assert_negative_api_retval(): + reply = self.vapi.cnat_set_snat_addresses_v2( + fwd_fib_index=0, + ret_fib_index=0xFFFFFFFF, + snat_ip4=snat_ip4, + snat_ip6=None, + sw_if_index=INVALID_INDEX, + flags=0, + ) + self.assertEqual(reply.retval, self.VNET_API_ERROR_NO_SUCH_FIB) + + # vpp is still there and the same call with valid fib indices goes + # through + reply = self.vapi.cnat_set_snat_addresses_v2( + fwd_fib_index=0, + ret_fib_index=0, + snat_ip4=snat_ip4, + snat_ip6=None, + sw_if_index=INVALID_INDEX, + flags=0, + ) + self.assertEqual(reply.retval, 0) + self.assertIn(snat_ip4, self.vapi.cli("show cnat snat-policy")) + + def test_snat_delete_only_named_fib_index(self): + """CNat Source Nat delete only removes the policy its fib index names""" + cli = self.vapi.cli + snat_ip4 = self.pg2.remote_hosts[0].ip4 + + # The lookup answers with the default policy when a fib index names no + # policy of its own, and the update then records that policy under the + # index, so the slots of several indexes end up naming one entry. The + # CLI marks the entry it creates for fib index 0 as the default policy. + self.vapi.cnat_set_snat_addresses(sw_if_index=INVALID_INDEX) + cli("set cnat snat-policy addr %s" % snat_ip4) + self.vapi.ip_table_add_del_v2(is_add=1, table={"table_id": 5}) + cli("set cnat snat-policy addr %s fib 1" % snat_ip4) + + # The delete of the index the entry was created for removes it, and + # every slot that named it has to be empty afterwards instead of naming + # the freed entry, so fib index 1 can be configured again. + reply = self.vapi.cnat_set_snat_addresses( + snat_ip4=None, snat_ip6=None, sw_if_index=INVALID_INDEX + ) + self.assertEqual(reply.retval, 0) + reply = self.vapi.cnat_set_snat_addresses_v2( + fwd_fib_index=1, + ret_fib_index=1, + snat_ip4=snat_ip4, + snat_ip6=None, + sw_if_index=INVALID_INDEX, + flags=0, + ) + self.assertEqual(reply.retval, 0) + + # A delete names one fib index and must not be answered with the + # policy of another one. + cli("set cnat snat-policy addr %s" % snat_ip4) + with self.vapi.assert_negative_api_retval(): + reply = self.vapi.cnat_set_snat_addresses_v2( + fwd_fib_index=0xFFFFFFFF, + ret_fib_index=0, + snat_ip4=None, + snat_ip6=None, + sw_if_index=INVALID_INDEX, + flags=0, + ) + self.assertEqual(reply.retval, self.VNET_API_ERROR_NO_SUCH_FIB) + self.assertIn(snat_ip4, cli("show cnat snat-policy")) + + def test_snat_refused_delete_keeps_the_watch(self): + """A refused CNat Source Nat delete leaves the policy alone""" + # vnet/cnat/cnat_snat_policy.h: CNAT_SNAT_POLICY_FLAG_USE_AS_DEFAULT + CNAT_SNAT_POLICY_FLAG_USE_AS_DEFAULT = 4 + + # The policy setUp configured holds fib index 0, so it has to go before + # one that is marked as the default policy can take its place. + self.vapi.cnat_set_snat_addresses(sw_if_index=INVALID_INDEX) + + # The policy is created for an interface that carries no address yet, + # so cnat_resolve_addr() fails and the endpoint is watched instead of + # being resolved right away. + loopback = self.create_loopback_interfaces(1)[0] + reply = self.vapi.cnat_set_snat_addresses_v2( + fwd_fib_index=0, + ret_fib_index=0, + snat_ip4=None, + snat_ip6=None, + sw_if_index=loopback.sw_if_index, + flags=CNAT_SNAT_POLICY_FLAG_USE_AS_DEFAULT, + ) + self.assertEqual(reply.retval, 0) + + # A delete names a fib index that holds no policy of its own, so the + # lookup answers with the default policy. The call has to be refused, + # and it may not have dropped the watch of that policy on the way. + with self.vapi.assert_negative_api_retval(): + reply = self.vapi.cnat_set_snat_addresses_v2( + fwd_fib_index=0xFFFFFFFF, + ret_fib_index=0, + snat_ip4=None, + snat_ip6=None, + sw_if_index=INVALID_INDEX, + flags=0, + ) + self.assertEqual(reply.retval, self.VNET_API_ERROR_NO_SUCH_FIB) + + # The watch still has to answer the address that appears now. + self.vapi.sw_interface_add_del_address( + sw_if_index=loopback.sw_if_index, is_add=1, prefix="10.199.0.1/24" + ) + self.assertIn("(10.199.0.1)", self.vapi.cli("show cnat snat-policy")) + @unittest.skipIf("cnat" in config.excluded_plugins, "Exclude CNAT plugin tests") class TestCNatDHCP(CnatCommonTestCase): From b941ccf1e120e58ad0e6dd073decbdf528eb3739 Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 22 Sep 2026 17:35:42 +0800 Subject: [PATCH 70/85] ipip: reject a 6RD tunnel in the generic delete path (#25) ipip_del_tunnel() looks the tunnel up by sw_if_index and tears down what a tunnel of mode P2P or P2MP owns, without ever looking at the mode. A 6RD tunnel owns IPv6 state as well: sixrd_add_tunnel() locks the table of the tunnel with FIB_SOURCE_6RD (sixrd.c:340), installs the route of the 6RD prefix in it, and enables IPv6 on the interface of the tunnel. sixrd_del_tunnel() undoes all three (sixrd.c:389, :394, :399). The generic delete path undoes none of them, so it removes the interface and the tunnel entry while the table keeps the FIB_SOURCE_6RD lock: a table is destroyed only when all of its locks went back to zero (fib_table.c:1441), and ip_table_delete() holds one lock of its own on the table (ip_api.c:585), so deleting the table through the API does not free it either. The tunnel is gone from the dumps at that point, so the leak cannot be undone. Reject IPIP_MODE_6RD in ipip_del_tunnel() with VNET_API_ERROR_INVALID_VALUE the way sixrd_del_tunnel() rejects the other modes, and leave the tunnel untouched. No caller of this path can ask for that mode: ipip_add_tunnel() maps its tunnel_mode_t argument to IPIP_MODE_P2P or IPIP_MODE_P2MP (ipip.c:759), and the API type it comes from carries TUNNEL_API_MODE_P2P and TUNNEL_API_MODE_MP only (tunnel_types.api:51, :53). A 6RD tunnel is created by ipip_6rd_add_tunnel and "create 6rd tunnel", and removed by ipip_6rd_del_tunnel and "delete 6rd tunnel". ikev2 is the one plugin that calls ipip_del_tunnel() through a function pointer (ikev2.c:2734); it looks its tunnels up with a key of mode 0, so it never reaches a 6RD tunnel. Type: fix Change-Id: I0000000000000000000000000000000000000010 Signed-off-by: Bo Xu --- src/plugins/ipip/ipip.c | 14 ++++++++++ test/test_ipip.py | 62 +++++++++++++++++++++++++++++++++++------ 2 files changed, 68 insertions(+), 8 deletions(-) diff --git a/src/plugins/ipip/ipip.c b/src/plugins/ipip/ipip.c index ed8f2a08935c..4cbffde93b2f 100644 --- a/src/plugins/ipip/ipip.c +++ b/src/plugins/ipip/ipip.c @@ -857,6 +857,20 @@ ipip_del_tunnel (u32 sw_if_index) if (t == NULL) return VNET_API_ERROR_NO_SUCH_ENTRY; + /* + * A 6RD tunnel carries IPv6 FIB state that only sixrd_del_tunnel() + * releases, and no caller of this function can ask for that mode: + * ipip_add_tunnel() maps its tunnel_mode_t argument to IPIP_MODE_P2P or + * IPIP_MODE_P2MP, and a 6RD tunnel is created by sixrd_add_tunnel() and + * deleted by sixrd_del_tunnel(). + */ + if (IPIP_MODE_6RD == t->mode) + { + clib_warning ("IPIP tunnel delete: tunnel is a 6RD tunnel: %d", + sw_if_index); + return VNET_API_ERROR_INVALID_VALUE; + } + if (t->mode == IPIP_MODE_P2MP) teib_walk_itf (t->sw_if_index, ipip_tunnel_delete_teib_walk, t); diff --git a/test/test_ipip.py b/test/test_ipip.py index 375f99abf326..c89217bec4ae 100644 --- a/test/test_ipip.py +++ b/test/test_ipip.py @@ -1611,6 +1611,25 @@ def setUp(self): self.ip4_table = VppIpTable(self, 4).add_vpp_config() self.ip6_table = VppIpTable(self, 6, True).add_vpp_config() + def add_6rd_tunnel(self): + """Add a 6RD tunnel in the two tables of setUp()""" + return self.vapi.ipip_6rd_add_tunnel( + ip6_table_id=self.ip6_table.table_id, + ip4_table_id=self.ip4_table.table_id, + ip6_prefix="2002::/16", + ip4_prefix="0.0.0.0/0", + ip4_src="10.0.0.1", + security_check=True, + ) + + def assert_ip6_table_freed(self): + """The ip6 table of setUp() is gone once its locks are released""" + self.ip6_table.remove_vpp_config() + self.assertNotIn( + self.ip6_table.table_id, + [t.table.table_id for t in self.vapi.ip_table_dump() if t.table.is_ip6], + ) + def test_sixrd_del_tunnel_rejects_plain_ipip_tunnel(self): """delete a plain IPIP tunnel through the 6RD entry point""" tunnel = ipip_add_tunnel(self, "1.1.1.1", "2.2.2.2", instance=100) @@ -1643,19 +1662,46 @@ def test_sixrd_del_tunnel_cli_rejects_plain_ipip_tunnel(self): def test_sixrd_del_tunnel_removes_6rd_tunnel(self): """delete a 6RD tunnel through the 6RD entry point""" - tunnel = self.vapi.ipip_6rd_add_tunnel( - ip6_table_id=self.ip6_table.table_id, - ip4_table_id=self.ip4_table.table_id, - ip6_prefix="2002::/16", - ip4_prefix="0.0.0.0/0", - ip4_src="10.0.0.1", - security_check=True, - ) + tunnel = self.add_6rd_tunnel() sw_if_index = tunnel.sw_if_index self.vapi.ipip_6rd_del_tunnel(sw_if_index=sw_if_index) self.assertEqual([], self.vapi.ipip_tunnel_dump(sw_if_index=sw_if_index)) + self.assert_ip6_table_freed() + + def test_ipip_del_tunnel_rejects_6rd_tunnel(self): + """delete a 6RD tunnel through the generic IPIP entry point""" + tunnel = self.add_6rd_tunnel() + sw_if_index = tunnel.sw_if_index + + with self.vapi.assert_known_api_retval( + retvals=[self.VNET_API_ERROR_INVALID_VALUE] + ): + self.vapi.ipip_del_tunnel(sw_if_index=sw_if_index) + + self.assertFalse(self.vpp_dead) + tunnels = self.vapi.ipip_tunnel_dump(sw_if_index=sw_if_index) + self.assertEqual(1, len(tunnels)) + self.assertEqual(sw_if_index, tunnels[0].tunnel.sw_if_index) + + self.vapi.ipip_6rd_del_tunnel(sw_if_index=sw_if_index) + self.assertEqual([], self.vapi.ipip_tunnel_dump(sw_if_index=sw_if_index)) + self.assert_ip6_table_freed() + + def test_ipip_del_tunnel_cli_rejects_6rd_tunnel(self): + """delete a 6RD tunnel through the generic IPIP CLI entry point""" + tunnel = self.add_6rd_tunnel() + sw_if_index = tunnel.sw_if_index + + self.vapi.cli("delete ipip tunnel sw_if_index %d" % sw_if_index) + + self.assertFalse(self.vpp_dead) + tunnels = self.vapi.ipip_tunnel_dump(sw_if_index=sw_if_index) + self.assertEqual(1, len(tunnels)) + + self.vapi.ipip_6rd_del_tunnel(sw_if_index=sw_if_index) + self.assertEqual([], self.vapi.ipip_tunnel_dump(sw_if_index=sw_if_index)) if __name__ == "__main__": From 47b829bc2c6f91e039902c49e80aeb57b6f17afd Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 22 Sep 2026 19:02:35 +0800 Subject: [PATCH 71/85] gha: add pull request verification on GitHub-hosted runners (#29) * gha: drop workflows that cannot run on this fork Sixteen of the seventeen workflows come from upstream FD.io and target its self-hosted Nomad runners (runs-on: [self-hosted, nomad, fdio:...]). They also consume the fdio/* remote composite actions and the AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, PACKAGECLOUD_TOKEN and FDIO_VPP_COVERITY_TOKEN secrets. This fork has no runners with those labels and no Actions secrets, so every one of those jobs sits queued forever: the periodic coverity and asan jobs were observed queued for over an hour with no runner ever picking them up. Only the Debian 13 DEB workflow runs on GitHub-hosted runners and completes. Remove the sixteen that cannot execute here so the workflow list shows only what this fork can actually run. Type: ci * gha: drop the unused Gerrit checkout action The vpp-checkout-gerrit-change composite action reuses the Gerrit-only refspec, branch and event inputs that the workflows removed in the previous commit supplied. Nothing references it any more, and this fork verifies GitHub pull requests, not Gerrit changes. Type: ci * gha: add a pull request verification workflow This fork has no pull request verification. The sixteen workflows removed earlier could not run here, and only the Debian 13 DEB build workflow executed on a GitHub-hosted runner. Add pr-verify.yml, which runs on GitHub-hosted runners and has a fast style job and a combined build-and-test job. The style job runs the repo's own gates (checkstyle, checkstyle-commit, checkfeaturelist, checkstyle-api and checkstyle-python) against the pull request's commit range. The build-and-test job installs the Debian 13 dependencies, builds VPP in debug mode with ccache keyed by commit the way the DEB workflow keys it, and runs the Python functional tests with a focused TEST filter derived from the changed paths, falling back to an ip4 smoke suite. A per-ref concurrency group cancels a superseded run. The temporary push trigger on ci/** exists only while the workflow is brought up and is removed before the pull request is opened. Type: ci * gha: fix bring-up failures in the PR workflow make checkstyle-api runs crcchecker.py, which refuses to run when a tracked file is modified. An earlier step edited the Makefile, so the working tree was dirty and the check failed with "Please stash or commit changes in workspace". The edit is not needed in this job, so drop it and leave the tree clean. The build failed at CMake configure with "VPP version : none": src/scripts/version calls git describe without --tags, and the origin of this fork has no v* tags. The DEB workflow already handles the same clone, so write src/scripts/.version in its format when no tag is reachable. Type: fix * gha: finalise the pull request verification workflow Drop the temporary push trigger that let a plain push to a ci/* branch run the workflow while it was brought up. The workflow now triggers on pull_request and workflow_dispatch only. Skip a cache save when its exact key was restored. Asking to save a key that already exists fails the step with "Unable to reserve cache", which happened on the second run against an unchanged Makefile. The keys themselves are unchanged. Type: ci --- .../vpp-checkout-gerrit-change/README.md | 34 -- .../vpp-checkout-gerrit-change/action.yml | 53 --- .github/workflows/periodic-vpp-coverity.yml | 125 ------- .../periodic-vpp-verify-asan-hst.yml | 181 ---------- .../periodic-vpp-verify-asan-maketest.yml | 178 ---------- .github/workflows/periodic-vpp-verify-cov.yml | 190 ---------- .../periodic-vpp-verify-dpdk-rdma-ver.yml | 138 -------- .github/workflows/periodic-vpp-verify-hst.yml | 186 ---------- .github/workflows/pr-verify.yml | 257 ++++++++++++++ .github/workflows/vpp-csit-verify-api.yml | 185 ---------- .github/workflows/vpp-merge-docs.yml | 215 ------------ .github/workflows/vpp-merge-maketest.yml | 330 ------------------ .github/workflows/vpp-verify-arm-drivers.yml | 224 ------------ .github/workflows/vpp-verify-checkstyle.yml | 266 -------------- .github/workflows/vpp-verify-docs.yml | 200 ----------- .github/workflows/vpp-verify-gcc.yml | 217 ------------ .github/workflows/vpp-verify-hst-u2204.yml | 252 ------------- .github/workflows/vpp-verify-hst.yml | 252 ------------- .github/workflows/vpp-verify-maketest.yml | 285 --------------- 19 files changed, 257 insertions(+), 3511 deletions(-) delete mode 100644 .github/actions/vpp-checkout-gerrit-change/README.md delete mode 100644 .github/actions/vpp-checkout-gerrit-change/action.yml delete mode 100644 .github/workflows/periodic-vpp-coverity.yml delete mode 100644 .github/workflows/periodic-vpp-verify-asan-hst.yml delete mode 100644 .github/workflows/periodic-vpp-verify-asan-maketest.yml delete mode 100644 .github/workflows/periodic-vpp-verify-cov.yml delete mode 100644 .github/workflows/periodic-vpp-verify-dpdk-rdma-ver.yml delete mode 100644 .github/workflows/periodic-vpp-verify-hst.yml create mode 100644 .github/workflows/pr-verify.yml delete mode 100644 .github/workflows/vpp-csit-verify-api.yml delete mode 100644 .github/workflows/vpp-merge-docs.yml delete mode 100644 .github/workflows/vpp-merge-maketest.yml delete mode 100644 .github/workflows/vpp-verify-arm-drivers.yml delete mode 100644 .github/workflows/vpp-verify-checkstyle.yml delete mode 100644 .github/workflows/vpp-verify-docs.yml delete mode 100644 .github/workflows/vpp-verify-gcc.yml delete mode 100644 .github/workflows/vpp-verify-hst-u2204.yml delete mode 100644 .github/workflows/vpp-verify-hst.yml delete mode 100644 .github/workflows/vpp-verify-maketest.yml diff --git a/.github/actions/vpp-checkout-gerrit-change/README.md b/.github/actions/vpp-checkout-gerrit-change/README.md deleted file mode 100644 index 550e755ad1c1..000000000000 --- a/.github/actions/vpp-checkout-gerrit-change/README.md +++ /dev/null @@ -1,34 +0,0 @@ -# 🛠️ VPP Checkout Gerrit Change Action - -Fetches a Gerrit patchset from `origin` and checks out `FETCH_HEAD` in an existing VPP workspace. - -## Inputs - -| Input | Required | Default | Notes | -| --- | --- | --- | --- | -| `WORKSPACE` | Yes | none | Absolute path to the checked out VPP tree | -| `GERRIT_BRANCH` | Yes | none | Branch to checkout before fetching the patchset | -| `GERRIT_REFSPEC` | Yes | none | Gerrit refspec to fetch from `origin` | -| `LOG_DIR` | No | `/scratch/docker-build/vpp/logs/checkout` | Directory used for `checkout.log` | -| `TUI_LINE` | No | `*******************************************************************` | Log separator | - -## What It Runs - -```bash -git checkout "$GERRIT_BRANCH" || echo "WARNING: Could not checkout $GERRIT_BRANCH!" -git fetch --tags origin "$GERRIT_REFSPEC" -git checkout FETCH_HEAD -git show --stat -``` - -## Usage - -```yaml -- name: Checkout Gerrit change - uses: ./.github/actions/vpp-checkout-gerrit-change - with: - WORKSPACE: ${{ github.workspace }} - GERRIT_BRANCH: ${{ inputs.GERRIT_BRANCH }} - GERRIT_REFSPEC: ${{ inputs.GERRIT_REFSPEC }} - LOG_DIR: ${{ github.workspace }}/logs/checkout -``` diff --git a/.github/actions/vpp-checkout-gerrit-change/action.yml b/.github/actions/vpp-checkout-gerrit-change/action.yml deleted file mode 100644 index b10c2d57d46b..000000000000 --- a/.github/actions/vpp-checkout-gerrit-change/action.yml +++ /dev/null @@ -1,53 +0,0 @@ ---- -name: "🛠️ VPP Checkout Gerrit Change" -description: | - This GitHub Action checks out the VPP Gerrit change specified by the input parameters. - -inputs: - LOG_DIR: - description: "Directory for checkout logs" - required: false - default: "/scratch/docker-build/vpp/logs/checkout" - type: string - WORKSPACE: - description: "Workspace directory where VPP is located" - required: true - type: string - GERRIT_BRANCH: - description: "Gerrit branch to checkout" - required: true - type: string - GERRIT_REFSPEC: - description: "Gerrit refspec to fetch" - required: true - type: string - TUI_LINE: - description: "Delimiter line for TUI output" - required: false - default: "*******************************************************************" - type: string - -runs: - using: "composite" - steps: - - name: Get Gerrit Change - shell: bash - env: - LOG_DIR: ${{ inputs.LOG_DIR }} - WORKSPACE: ${{ inputs.WORKSPACE }} - GERRIT_BRANCH: ${{ inputs.GERRIT_BRANCH }} - GERRIT_REFSPEC: ${{ inputs.GERRIT_REFSPEC }} - TUI_LINE: ${{ inputs.TUI_LINE }} - run: | - set -euxo pipefail - mkdir -p "$LOG_DIR" - { - echo "$TUI_LINE" - echo "* Checkout $GERRIT_BRANCH ($GERRIT_REFSPEC)" - echo "$TUI_LINE" - cd "$WORKSPACE" - git checkout "$GERRIT_BRANCH" || echo "WARNING: Could not checkout $GERRIT_BRANCH!" - git fetch --all origin "$GERRIT_REFSPEC" - git checkout FETCH_HEAD - git show --stat - } | tee "$LOG_DIR/checkout.log" diff --git a/.github/workflows/periodic-vpp-coverity.yml b/.github/workflows/periodic-vpp-coverity.yml deleted file mode 100644 index 51ff2a5e3a14..000000000000 --- a/.github/workflows/periodic-vpp-coverity.yml +++ /dev/null @@ -1,125 +0,0 @@ -name: periodic-vpp-coverity - -on: - schedule: - - cron: '0 3 * * *' # Runs daily at 3am UTC - workflow_dispatch: - inputs: - FDIO_NAMESPACE: - description: "Executor Namespace (prod|sandbox)" - required: false - type: choice - options: - - sandbox - - prod - default: prod - -defaults: - run: - shell: bash - -permissions: - contents: read - actions: read - -concurrency: - group: ${{ github.workflow }}-${{ inputs.FDIO_NAMESPACE }} - cancel-in-progress: true - -jobs: - coverity-build: - # yamllint disable-line rule:line-length - name: coverity-build-${{ matrix.os }}-${{ matrix.executor_arch }}-${{ github.event.inputs.FDIO_NAMESPACE || 'prod' }} - runs-on: - - self-hosted - - nomad - - fdio:arch=${{ matrix.executor_arch }} - - fdio:class=${{ matrix.class }} - - fdio:namespace=${{ github.event.inputs.FDIO_NAMESPACE || 'prod' }} - - fdio:os=${{ matrix.os }} - - fdio:workflow=${{ github.run_id }} - - strategy: - fail-fast: false - matrix: - build_type: ['debug'] - class: ['builder'] - os: ['ubuntu2404'] - executor_arch: ['x86_64'] - - env: - FDIO_NAMESPACE: ${{ github.event.inputs.FDIO_NAMESPACE || 'prod' }} - LOG_NAME: "" - LOG_DIR: "" - WORKSPACE: /scratch/docker-build/vpp - TUI_LINE: "*******************************************************************" - - steps: - - name: Actions Checkout - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v6.0.2 - - - name: Setup Environment - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/setup-executor-env@b728d1589228e2891a4241a1bba362d76895c225 - - - name: Timestamp and Log Directory - run: | - set -euxo pipefail - ts="$(date -u +%Y_%m_%d_%H%M%S_UTC)" - LOG_NAME="coverity-build-master-${ts}-${{ matrix.build_type }}-${{ matrix.class }}-${{ matrix.os }}-${{ env.FDIO_NAMESPACE }}-${{ matrix.executor_arch }}" - echo "LOG_NAME=$LOG_NAME" >> "$GITHUB_ENV" - LOG_DIR="$WORKSPACE/logs/$LOG_NAME" - echo "LOG_DIR=$LOG_DIR" >> "$GITHUB_ENV" - mkdir -p "$LOG_DIR" - - - name: Show Environment - run: | - set -euo pipefail - env | sort | tee "$LOG_DIR/environment.log" - - - name: Show Nomad Dockerfile - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/show-nomad-dockerfile@b728d1589228e2891a4241a1bba362d76895c225 - - - name: Pull VPP master HEAD - run: | - set -euxo pipefail - cd "$WORKSPACE" - git checkout master - git pull - git show --stat - - - name: Install VPP dependencies - # yamllint disable-line rule:line-length - uses: fdio/vpp/.github/actions/vpp-install-deps@777792ecef2d5c93ce15fa5b7fdca999f5412c9d - with: - LOG_DIR: ${{ env.LOG_DIR }} - WORKSPACE: ${{ env.WORKSPACE }} - TUI_LINE: ${{ env.TUI_LINE }} - - - name: Install VPP external dependencies - # yamllint disable-line rule:line-length - uses: fdio/vpp/.github/actions/vpp-install-ext-deps@d0577011fb512d79e6914b6f1f11e2be4c09bc1e - with: - LOG_DIR: ${{ env.LOG_DIR }} - WORKSPACE: ${{ env.WORKSPACE }} - TUI_LINE: ${{ env.TUI_LINE }} - - - name: Run Coverity Build Script - run: | - set -euo pipefail - - cd $WORKSPACE - COVERITY_TOKEN=${{ secrets.FDIO_VPP_COVERITY_TOKEN }} $WORKSPACE/extras/scripts/coverity-build | tee $LOG_DIR/coverity-build.log - - - name: AWS S3 Publish Logs - if: ${{ always() }} - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/aws-s3-publish-logs@1b8eff3ffd9d355b28052be9ad22acc57b3430b5 - with: - aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws_region: ${{ vars.AWS_REGION }} - s3_bucket: ${{ vars.S3_BUCKET }} - s3_path: gha-vpp-periodic/${{ env.LOG_NAME }} - archives_path: ${{ env.LOG_DIR }} diff --git a/.github/workflows/periodic-vpp-verify-asan-hst.yml b/.github/workflows/periodic-vpp-verify-asan-hst.yml deleted file mode 100644 index 2f57912b82e0..000000000000 --- a/.github/workflows/periodic-vpp-verify-asan-hst.yml +++ /dev/null @@ -1,181 +0,0 @@ -name: periodic-vpp-verify-asan-hst - -on: - schedule: - - cron: '0 4 * * *' # Runs daily at 4am UTC - workflow_dispatch: - inputs: - FDIO_NAMESPACE: - description: "Executor Namespace (prod|sandbox)" - required: false - type: choice - options: - - sandbox - - prod - default: prod - -defaults: - run: - shell: bash - -permissions: - contents: read - actions: read - -concurrency: - group: ${{ github.workflow }}-${{ inputs.FDIO_NAMESPACE }} - cancel-in-progress: true - -jobs: - verify-asan-hst-periodic: - # yamllint disable-line rule:line-length - name: asan-hst-${{ matrix.os }}-${{ matrix.executor_arch }}-${{ matrix.build_type }}-${{ github.event.inputs.FDIO_NAMESPACE || 'prod' }} - runs-on: - - self-hosted - - nomad - - fdio:arch=${{ matrix.executor_arch }} - - fdio:class=${{ matrix.class }} - - fdio:size=${{ matrix.size }} - - fdio:namespace=${{ github.event.inputs.FDIO_NAMESPACE || 'prod' }} - - fdio:os=${{ matrix.os }} - - fdio:workflow=${{ github.run_id }} - - strategy: - fail-fast: false - matrix: - include: - # Debug builds on x86_64 - - build_type: 'debug' - os: 'ubuntu2404' - executor_arch: 'x86_64' - class: 'hst' - size: 'hst' - - env: - FDIO_NAMESPACE: ${{ github.event.inputs.FDIO_NAMESPACE || 'prod' }} - LOG_NAME: "" - LOG_DIR: "" - WORKSPACE: /scratch/docker-build/vpp - HS_TEST_DIR: /scratch/docker-build/vpp/test/hs-test - CCACHE_DIR: /scratch/ccache/${{ matrix.os }}-${{ matrix.executor_arch }} - MAKE_PARALLEL_JOBS: 16 - TEST_JOBS: "auto" - TUI_LINE: "*******************************************************************" - - steps: - - name: Actions Checkout - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v6.0.2 - - - name: Setup Environment - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/setup-executor-env@b728d1589228e2891a4241a1bba362d76895c225 - - - name: Set Timestamp and Log Path - run: | - set -euxo pipefail - ts="$(date -u +%Y_%m_%d_%H%M%S_UTC)" - LOG_NAME="gha-vpp-periodic/vpp-verify-asan-hst-${ts}-${{ matrix.build_type }}-${{ matrix.class }}-${{ matrix.os }}-${{ env.FDIO_NAMESPACE }}-${{ matrix.executor_arch }}" - echo "LOG_NAME=$LOG_NAME" >> "$GITHUB_ENV" - LOG_DIR="$WORKSPACE/logs/$LOG_NAME" - echo "LOG_DIR=$LOG_DIR" >> "$GITHUB_ENV" - mkdir -p "$LOG_DIR" - - - name: Show Environment - run: | - set -euo pipefail - env | sort | tee "$LOG_DIR/environment.log" - - - name: Show Nomad Dockerfile - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/show-nomad-dockerfile@b728d1589228e2891a4241a1bba362d76895c225 - - - name: Pull VPP master HEAD - run: | - set -euxo pipefail - cd "$WORKSPACE" - git checkout master - git pull - git show --stat - - - name: Install VPP dependencies - # yamllint disable-line rule:line-length - uses: fdio/vpp/.github/actions/vpp-install-deps@777792ecef2d5c93ce15fa5b7fdca999f5412c9d - with: - LOG_DIR: ${{ env.LOG_DIR }} - WORKSPACE: ${{ env.WORKSPACE }} - TUI_LINE: ${{ env.TUI_LINE }} - - - name: Install VPP external dependencies - # yamllint disable-line rule:line-length - uses: fdio/vpp/.github/actions/vpp-install-ext-deps@d0577011fb512d79e6914b6f1f11e2be4c09bc1e - with: - LOG_DIR: ${{ env.LOG_DIR }} - WORKSPACE: ${{ env.WORKSPACE }} - TUI_LINE: ${{ env.TUI_LINE }} - - - name: ASAN HST VPP Build - # yamllint disable-line rule:line-length - uses: fdio/vpp/.github/actions/vpp-build@72cac6c0bc22441f24ca057a1be8919a083c578f - timeout-minutes: 90 - with: - LOG_DIR: ${{ env.LOG_DIR }} - WORKSPACE: ${{ env.WORKSPACE }} - MAKE_PARALLEL_JOBS: ${{ env.MAKE_PARALLEL_JOBS }} - BUILD_TYPE: ${{ matrix['build_type'] }} - BUILD_HST: 'true' - ENABLE_ASAN: 'true' - TUI_LINE: ${{ env.TUI_LINE }} - - - name: Start Docker Daemon - uses: fdio/vpp/.github/actions/vpp-start-docker@f9e0f384d665800c8e580da853e26558dd6d2668 - with: - START_DOCKER_SCRIPT: start-docker.sh - TUI_LINE: ${{ env.TUI_LINE }} - - - name: ASAN HST Test VPP - if: success() - uses: fdio/vpp/.github/actions/vpp-hst-test@5c0def01c0f8a17f90a1b16c9039077e298e0adc - timeout-minutes: 180 - with: - LOG_DIR: ${{ env.LOG_DIR }} - WORKSPACE: ${{ env.WORKSPACE }} - BUILD_TYPE: ${{ matrix['build_type'] }} - HS_TEST_DIR: ${{ env.HS_TEST_DIR }} - TEST_JOBS: ${{ env.TEST_JOBS }} - TUI_LINE: ${{ env.TUI_LINE }} - - - name: AWS S3 Publish Logs - if: always() - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/aws-s3-publish-logs@1b8eff3ffd9d355b28052be9ad22acc57b3430b5 - with: - aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws_region: ${{ vars.AWS_REGION }} - s3_bucket: ${{ vars.S3_BUCKET }} - s3_path: ${{ env.LOG_NAME }} - archives_path: ${{ env.LOG_DIR }} - - - name: Check Archives Directory - id: check-archives - if: always() - run: | - set -euxo pipefail - archives_dir="$WORKSPACE/archives" - if [[ -d "$archives_dir" ]] && find "$archives_dir" -type f -print -quit | grep -q . ; then - echo "has_archives=true" >> "$GITHUB_OUTPUT" - else - echo "has_archives=false" >> "$GITHUB_OUTPUT" - fi - - - name: AWS S3 Publish Archives - if: ${{ always() && steps.check-archives.outputs.has_archives == 'true' }} - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/aws-s3-publish-logs@1b8eff3ffd9d355b28052be9ad22acc57b3430b5 - with: - aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws_region: ${{ vars.AWS_REGION }} - s3_bucket: ${{ vars.S3_BUCKET }} - s3_path: ${{ env.LOG_NAME }} - archives_path: ${{ env.WORKSPACE }}/archives diff --git a/.github/workflows/periodic-vpp-verify-asan-maketest.yml b/.github/workflows/periodic-vpp-verify-asan-maketest.yml deleted file mode 100644 index ed23ee85e730..000000000000 --- a/.github/workflows/periodic-vpp-verify-asan-maketest.yml +++ /dev/null @@ -1,178 +0,0 @@ -name: periodic-vpp-verify-asan-maketest - -on: - schedule: - - cron: '0 4 * * *' # Runs daily at 4am UTC - workflow_dispatch: - inputs: - FDIO_NAMESPACE: - description: "Executor Namespace (prod|sandbox)" - required: false - type: choice - options: - - sandbox - - prod - default: prod - -defaults: - run: - shell: bash - -permissions: - contents: read - actions: read - -concurrency: - group: ${{ github.workflow }}-${{ inputs.FDIO_NAMESPACE }} - cancel-in-progress: true - -jobs: - verify-asan-maketest-periodic: - # yamllint disable-line rule:line-length - name: asan-maketest-${{ matrix.os }}-${{ matrix.executor_arch }}-${{ matrix.build_type }}-${{ github.event.inputs.FDIO_NAMESPACE || 'prod' }} - runs-on: - - self-hosted - - nomad - - fdio:arch=${{ matrix.executor_arch }} - - fdio:class=${{ matrix.class }} - - fdio:namespace=${{ github.event.inputs.FDIO_NAMESPACE || 'prod' }} - - fdio:os=${{ matrix.os }} - - fdio:workflow=${{ github.run_id }} - - strategy: - fail-fast: false - matrix: - include: - # Debug builds on x86_64 - - build_type: 'debug' - os: 'ubuntu2404' - executor_arch: 'x86_64' - class: 'builder' - - env: - FDIO_NAMESPACE: ${{ github.event.inputs.FDIO_NAMESPACE || 'prod' }} - LOG_NAME: "" - LOG_DIR: "" - WORKSPACE: /scratch/docker-build/vpp - CCACHE_DIR: /scratch/ccache/${{ matrix.os }}-${{ matrix.executor_arch }} - MAKE_PARALLEL_JOBS: 16 - MAKE_TEST_OS: 'ubuntu-24.04' - TEST_JOBS: 4 - TUI_LINE: "*******************************************************************" - - steps: - - name: Actions Checkout - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v6.0.2 - - - name: Setup Environment - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/setup-executor-env@b728d1589228e2891a4241a1bba362d76895c225 - - - name: Set Timestamp and Log Path - run: | - set -euxo pipefail - ts="$(date -u +%Y_%m_%d_%H%M%S_UTC)" - LOG_NAME="gha-vpp-periodic/vpp-verify-asan-maketest-master-${ts}-${{ matrix.build_type }}-${{ matrix.class }}-${{ matrix.os }}-${{ env.FDIO_NAMESPACE }}-${{ matrix.executor_arch }}" - echo "LOG_NAME=$LOG_NAME" >> "$GITHUB_ENV" - LOG_DIR="$WORKSPACE/logs/$LOG_NAME" - echo "LOG_DIR=$LOG_DIR" >> "$GITHUB_ENV" - mkdir -p "$LOG_DIR" - - - name: Show Environment - run: | - set -euo pipefail - env | sort | tee "$LOG_DIR/environment.log" - - - name: Show Nomad Dockerfile - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/show-nomad-dockerfile@b728d1589228e2891a4241a1bba362d76895c225 - - - name: Pull VPP master HEAD - run: | - set -euxo pipefail - cd "$WORKSPACE" - git checkout master - git pull - git show --stat - - - name: Install VPP dependencies - # yamllint disable-line rule:line-length - uses: fdio/vpp/.github/actions/vpp-install-deps@777792ecef2d5c93ce15fa5b7fdca999f5412c9d - with: - LOG_DIR: ${{ env.LOG_DIR }} - WORKSPACE: ${{ env.WORKSPACE }} - TUI_LINE: ${{ env.TUI_LINE }} - - - name: Install VPP external dependencies - # yamllint disable-line rule:line-length - uses: fdio/vpp/.github/actions/vpp-install-ext-deps@d0577011fb512d79e6914b6f1f11e2be4c09bc1e - with: - LOG_DIR: ${{ env.LOG_DIR }} - WORKSPACE: ${{ env.WORKSPACE }} - TUI_LINE: ${{ env.TUI_LINE }} - - - name: ASAN MAKETEST VPP Build - # yamllint disable-line rule:line-length - uses: fdio/vpp/.github/actions/vpp-build@72cac6c0bc22441f24ca057a1be8919a083c578f - timeout-minutes: 90 - with: - LOG_DIR: ${{ env.LOG_DIR }} - WORKSPACE: ${{ env.WORKSPACE }} - MAKE_PARALLEL_JOBS: ${{ env.MAKE_PARALLEL_JOBS }} - BUILD_TYPE: ${{ matrix['build_type'] }} - ENABLE_ASAN: 'true' - TUI_LINE: ${{ env.TUI_LINE }} - - - name: Start Docker Daemon - uses: fdio/vpp/.github/actions/vpp-start-docker@f9e0f384d665800c8e580da853e26558dd6d2668 - with: - START_DOCKER_SCRIPT: start-docker.sh - TUI_LINE: ${{ env.TUI_LINE }} - - - name: ASAN MAKETEST VPP - if: success() - uses: fdio/vpp/.github/actions/vpp-make-test@03f35503fee57df2348617498db28fde557a3880 - timeout-minutes: 180 - with: - LOG_DIR: ${{ env.LOG_DIR }} - WORKSPACE: ${{ env.WORKSPACE }} - BUILD_TYPE: ${{ matrix.build_type }} - TEST_JOBS: ${{ env.TEST_JOBS }} - MAKE_TEST_OS: ${{ env.MAKE_TEST_OS }} - TUI_LINE: ${{ env.TUI_LINE }} - - - name: AWS S3 Publish Logs - if: always() - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/aws-s3-publish-logs@1b8eff3ffd9d355b28052be9ad22acc57b3430b5 - with: - aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws_region: ${{ vars.AWS_REGION }} - s3_bucket: ${{ vars.S3_BUCKET }} - s3_path: ${{ env.LOG_NAME }} - archives_path: ${{ env.LOG_DIR }} - - - name: Check Archives Directory - id: check-archives - if: always() - run: | - set -euxo pipefail - archives_dir="$WORKSPACE/archives" - if [[ -d "$archives_dir" ]] && find "$archives_dir" -type f -print -quit | grep -q . ; then - echo "has_archives=true" >> "$GITHUB_OUTPUT" - else - echo "has_archives=false" >> "$GITHUB_OUTPUT" - fi - - - name: AWS S3 Publish Archives - if: ${{ always() && steps.check-archives.outputs.has_archives == 'true' }} - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/aws-s3-publish-logs@1b8eff3ffd9d355b28052be9ad22acc57b3430b5 - with: - aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws_region: ${{ vars.AWS_REGION }} - s3_bucket: ${{ vars.S3_BUCKET }} - s3_path: ${{ env.LOG_NAME }} - archives_path: ${{ env.WORKSPACE }}/archives diff --git a/.github/workflows/periodic-vpp-verify-cov.yml b/.github/workflows/periodic-vpp-verify-cov.yml deleted file mode 100644 index fcab75cafaf9..000000000000 --- a/.github/workflows/periodic-vpp-verify-cov.yml +++ /dev/null @@ -1,190 +0,0 @@ -name: periodic-vpp-verify-cov - -on: - schedule: - - cron: '0 0 * * *' # Runs daily at midnight UTC - workflow_dispatch: - inputs: - FDIO_NAMESPACE: - description: "Executor Namespace (prod|sandbox)" - required: false - type: choice - options: - - sandbox - - prod - default: prod - -defaults: - run: - shell: bash - -permissions: - contents: read - actions: read - -concurrency: - group: ${{ github.workflow }}-${{ inputs.FDIO_NAMESPACE }} - cancel-in-progress: true - -jobs: - verify-cov: - # yamllint disable-line rule:line-length - name: cov-${{ matrix.os }}-${{ matrix.executor_arch }}-${{ github.event.inputs.FDIO_NAMESPACE || 'prod' }} - runs-on: - - self-hosted - - nomad - - fdio:arch=${{ matrix.executor_arch }} - - fdio:class=${{ matrix.class }} - - fdio:namespace=${{ github.event.inputs.FDIO_NAMESPACE || 'prod' }} - - fdio:os=${{ matrix.os }} - - fdio:workflow=${{ github.run_id }} - - strategy: - fail-fast: false - matrix: - build_type: ['release'] - class: ['builder'] - os: ['ubuntu2404'] - executor_arch: ['x86_64'] - - env: - FDIO_NAMESPACE: ${{ github.event.inputs.FDIO_NAMESPACE || 'prod' }} - LOG_NAME: "" - LOG_DIR: "" - S3_DOCS_7DAY_PATH: "" - S3_DOCS_7DAY_URL: https://s3-docs-7day.fd.io/vex-yul-rot-jenkins-1 - WORKSPACE: /scratch/docker-build/vpp - MAKE_PARALLEL_JOBS: 16 - TEST_JOBS: 4 - HS_PARALLEL: auto - TEST_RETRIES: 3 - MAKE_TEST_OS: ubuntu-24.04 - VPPAPIGEN_TEST_OS: ubuntu-24.04 - SHM_SIZE: 2048M - TUI_LINE: "*******************************************************************" - - steps: - - name: Actions Checkout - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v6.0.2 - - - name: Setup Environment - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/setup-executor-env@b728d1589228e2891a4241a1bba362d76895c225 - - - name: Set Timestamp and Log Path - run: | - set -euxo pipefail - ts="$(date -u +%Y_%m_%d_%H%M%S_UTC)" - LOG_NAME="gha-vpp-periodic/vpp-coverage-master-${ts}-${{ matrix.build_type }}-${{ matrix.class }}-${{ matrix.os }}-${{ env.FDIO_NAMESPACE }}-${{ matrix.executor_arch }}" - echo "LOG_NAME=$LOG_NAME" >> "$GITHUB_ENV" - echo "S3_DOCS_7DAY_PATH=$LOG_NAME/docs" >> "$GITHUB_ENV" - LOG_DIR="$WORKSPACE/logs/$LOG_NAME" - echo "LOG_DIR=$LOG_DIR" >> "$GITHUB_ENV" - mkdir -p "$LOG_DIR" - - - name: Show Environment - run: | - set -euo pipefail - env | sort | tee "$LOG_DIR/environment.log" - - - name: Show Nomad Dockerfile - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/show-nomad-dockerfile@b728d1589228e2891a4241a1bba362d76895c225 - - - name: Pull VPP master HEAD - run: | - set -euxo pipefail - cd "$WORKSPACE" - git checkout master - git pull - git show --stat - - - name: Install VPP dependencies - # yamllint disable-line rule:line-length - uses: fdio/vpp/.github/actions/vpp-install-deps@777792ecef2d5c93ce15fa5b7fdca999f5412c9d - with: - LOG_DIR: ${{ env.LOG_DIR }} - WORKSPACE: ${{ env.WORKSPACE }} - TUI_LINE: ${{ env.TUI_LINE }} - - - name: Install VPP external dependencies - # yamllint disable-line rule:line-length - uses: fdio/vpp/.github/actions/vpp-install-ext-deps@d0577011fb512d79e6914b6f1f11e2be4c09bc1e - with: - LOG_DIR: ${{ env.LOG_DIR }} - WORKSPACE: ${{ env.WORKSPACE }} - TUI_LINE: ${{ env.TUI_LINE }} - - - name: Setup Docker VPP Runtime Environment for 'make test' - # yamllint disable-line rule:line-length - uses: fdio/vpp/.github/actions/vpp-docker-runtime-setup@777792ecef2d5c93ce15fa5b7fdca999f5412c9d - with: - LOG_DIR: ${{ env.LOG_DIR }} - WORKSPACE: ${{ env.WORKSPACE }} - SHM_SIZE: ${{ env.SHM_SIZE }} - TUI_LINE: ${{ env.TUI_LINE }} - - - name: Start Docker Daemon - uses: fdio/vpp/.github/actions/vpp-start-docker@f9e0f384d665800c8e580da853e26558dd6d2668 - with: - START_DOCKER_SCRIPT: start-docker.sh - TUI_LINE: ${{ env.TUI_LINE }} - - - name: VPP Make Coverage Test - run: | - cd "$WORKSPACE" - set -x - { - make COMPRESS_FAILED_TEST_LOGS=yes TEST_JOBS=$TEST_JOBS \ - HS_PARALLEL=$HS_PARALLEL CCACHE_DISABLE=1 test-cov-both || true - } | tee "$LOG_DIR/make_coverage_test.log" - - - name: AWS S3 Publish Logs - if: ${{ always() }} - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/aws-s3-publish-logs@1b8eff3ffd9d355b28052be9ad22acc57b3430b5 - with: - aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws_region: ${{ vars.AWS_REGION }} - s3_bucket: ${{ vars.S3_BUCKET }} - s3_path: ${{ env.LOG_NAME }} - archives_path: ${{ env.LOG_DIR }} - - - name: AWS S3 Publish Coverage Report - if: ${{ always() }} - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/aws-s3-publish-logs@1b8eff3ffd9d355b28052be9ad22acc57b3430b5 - with: - aws_access_key_id: ${{ secrets.AWS_DOCS_7DAY_ACCESS_KEY_ID }} - aws_secret_access_key: ${{ secrets.AWS_DOCS_7DAY_SECRET_ACCESS_KEY }} - aws_region: ${{ vars.AWS_REGION }} - s3_bucket: ${{ vars.S3_DOCS_7DAY_BUCKET}} - s3_path: ${{ env.S3_DOCS_7DAY_PATH }} - s3_url: ${{ env.S3_DOCS_7DAY_URL }} - archives_path: "${{ env.WORKSPACE }}/build-root/test-coverage-merged/html" - compress_files: false - - - name: Check Archives Directory - id: check-archives - if: always() - run: | - set -euxo pipefail - archives_dir="$WORKSPACE/archives" - if [[ -d "$archives_dir" ]] && find "$archives_dir" -type f -print -quit | grep -q . ; then - echo "has_archives=true" >> "$GITHUB_OUTPUT" - else - echo "has_archives=false" >> "$GITHUB_OUTPUT" - fi - - - name: AWS S3 Publish Archives - if: ${{ always() && steps.check-archives.outputs.has_archives == 'true' }} - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/aws-s3-publish-logs@1b8eff3ffd9d355b28052be9ad22acc57b3430b5 - with: - aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws_region: ${{ vars.AWS_REGION }} - s3_bucket: ${{ vars.S3_BUCKET }} - s3_path: ${{ env.LOG_NAME }} - archives_path: ${{ env.WORKSPACE }}/archives diff --git a/.github/workflows/periodic-vpp-verify-dpdk-rdma-ver.yml b/.github/workflows/periodic-vpp-verify-dpdk-rdma-ver.yml deleted file mode 100644 index b54ffb653127..000000000000 --- a/.github/workflows/periodic-vpp-verify-dpdk-rdma-ver.yml +++ /dev/null @@ -1,138 +0,0 @@ -name: periodic-vpp-verify-dpdk-rdma-ver - -on: - schedule: - - cron: '0 2 * * *' # Runs daily at 2am UTC - workflow_dispatch: - inputs: - FDIO_NAMESPACE: - description: "Executor Namespace (prod|sandbox)" - required: false - type: choice - options: - - sandbox - - prod - default: prod - -defaults: - run: - shell: bash - -permissions: - contents: read - actions: read - -concurrency: - group: ${{ github.workflow }}-${{ inputs.FDIO_NAMESPACE }} - cancel-in-progress: true - -jobs: - verify-dpdk-rdma-version: - # yamllint disable-line rule:line-length - name: dpdk-rdma-ver-${{ matrix.os }}-${{ matrix.executor_arch }}-${{ github.event.inputs.FDIO_NAMESPACE || 'prod' }} - runs-on: - - self-hosted - - nomad - - fdio:arch=${{ matrix.executor_arch }} - - fdio:class=${{ matrix.class }} - - fdio:namespace=${{ github.event.inputs.FDIO_NAMESPACE || 'prod' }} - - fdio:os=${{ matrix.os }} - - fdio:workflow=${{ github.run_id }} - - strategy: - fail-fast: false - matrix: - build_type: ['release'] - class: ['builder'] - os: ['ubuntu2404'] - executor_arch: ['x86_64'] - - env: - FDIO_NAMESPACE: ${{ github.event.inputs.FDIO_NAMESPACE || 'prod' }} - LOG_NAME: "" - LOG_DIR: "" - WORKSPACE: /scratch/docker-build/vpp - TUI_LINE: "*******************************************************************" - - steps: - - name: Actions Checkout - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v6.0.2 - - - name: Setup Environment - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/setup-executor-env@b728d1589228e2891a4241a1bba362d76895c225 - - - name: Timestamp and Log Directory - run: | - set -euxo pipefail - ts="$(date -u +%Y_%m_%d_%H%M%S_UTC)" - LOG_NAME="dpdk-rdma-version-master-${ts}-${{ matrix.build_type }}-${{ matrix.class }}-${{ matrix.os }}-${{ env.FDIO_NAMESPACE }}-${{ matrix.executor_arch }}" - echo "LOG_NAME=$LOG_NAME" >> "$GITHUB_ENV" - LOG_DIR="$WORKSPACE/logs/$LOG_NAME" - echo "LOG_DIR=$LOG_DIR" >> "$GITHUB_ENV" - mkdir -p "$LOG_DIR" - - - name: Show Environment - run: | - set -euo pipefail - env | sort | tee "$LOG_DIR/environment.log" - - - name: Show Nomad Dockerfile - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/show-nomad-dockerfile@b728d1589228e2891a4241a1bba362d76895c225 - - - name: Pull VPP master HEAD - run: | - set -euxo pipefail - cd "$WORKSPACE" - git checkout master - git pull - git show --stat - - - name: Install VPP dependencies - # yamllint disable-line rule:line-length - uses: fdio/vpp/.github/actions/vpp-install-deps@777792ecef2d5c93ce15fa5b7fdca999f5412c9d - with: - LOG_DIR: ${{ env.LOG_DIR }} - WORKSPACE: ${{ env.WORKSPACE }} - TUI_LINE: ${{ env.TUI_LINE }} - - - name: Install VPP external dependencies - # yamllint disable-line rule:line-length - uses: fdio/vpp/.github/actions/vpp-install-ext-deps@d0577011fb512d79e6914b6f1f11e2be4c09bc1e - with: - LOG_DIR: ${{ env.LOG_DIR }} - WORKSPACE: ${{ env.WORKSPACE }} - TUI_LINE: ${{ env.TUI_LINE }} - - - name: Build VPP and Verify DPDK RDMA driver - run: | - set -euo pipefail - - EXTERNAL_BUILD_DIR="$WORKSPACE/build/external" - RETVAL="0" - RESULT="pass" - MISMATCH_RESULT="INCLUDED IN" - - make -C "$EXTERNAL_BUILD_DIR" build-deb | tee "$LOG_DIR/build_deb.log" - source "$EXTERNAL_BUILD_DIR/dpdk_mlx_default.sh" || true - - if [ "${DPDK_MLX_DEFAULT-}" = "n" ] ; then - MISMATCH_RESULT="MISSING FROM" - RETVAL="1" - RESULT="fail" - fi - echo -e "\n$TUI_LINE\n* MLX DPDK DRIVER $MISMATCH_RESULT VPP-EXT-DEPS PACKAGE\n$TUI_LINE\n" | tee "$LOG_DIR/result_${RESULT}.log" - exit $RETVAL - - - name: AWS S3 Publish Logs - if: ${{ always() }} - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/aws-s3-publish-logs@1b8eff3ffd9d355b28052be9ad22acc57b3430b5 - with: - aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws_region: ${{ vars.AWS_REGION }} - s3_bucket: ${{ vars.S3_BUCKET }} - s3_path: gha-vpp-periodic/${{ env.LOG_NAME }} - archives_path: ${{ env.LOG_DIR }} diff --git a/.github/workflows/periodic-vpp-verify-hst.yml b/.github/workflows/periodic-vpp-verify-hst.yml deleted file mode 100644 index aa45d6b46412..000000000000 --- a/.github/workflows/periodic-vpp-verify-hst.yml +++ /dev/null @@ -1,186 +0,0 @@ -name: periodic-vpp-verify-hst - -on: - schedule: - - cron: '0 1 * * *' # Runs daily at 1am UTC - - cron: '0 13 * * *' # Runs daily at 1pm UTC - workflow_dispatch: - inputs: - FDIO_NAMESPACE: - description: "Executor Namespace (prod|sandbox)" - required: false - type: choice - options: - - sandbox - - prod - default: prod - -defaults: - run: - shell: bash - -permissions: - contents: read - actions: read - -concurrency: - group: ${{ github.workflow }}-${{ inputs.FDIO_NAMESPACE }} - cancel-in-progress: true - -jobs: - verify-hst-periodic: - # yamllint disable-line rule:line-length - name: hst-${{ matrix.os }}-${{ matrix.executor_arch }}-${{ matrix.build_type }}-${{ github.event.inputs.FDIO_NAMESPACE || 'prod' }} - runs-on: - - self-hosted - - nomad - - fdio:arch=${{ matrix.executor_arch }} - - fdio:class=${{ matrix.class }} - - fdio:size=${{ matrix.size }} - - fdio:namespace=${{ github.event.inputs.FDIO_NAMESPACE || 'prod' }} - - fdio:os=${{ matrix.os }} - - fdio:workflow=${{ github.run_id }} - - strategy: - fail-fast: false - matrix: - include: - # Debug builds on aarch64 - - build_type: 'debug' - os: 'ubuntu2204' - executor_arch: 'aarch64' - class: 'hst' - size: 'hst' - # Debug builds on x86_64 - - build_type: 'debug' - os: 'ubuntu2204' - executor_arch: 'x86_64' - class: 'hst' - size: 'hst' - - env: - FDIO_NAMESPACE: ${{ github.event.inputs.FDIO_NAMESPACE || 'prod' }} - LOG_NAME: "" - LOG_DIR: "" - WORKSPACE: /scratch/docker-build/vpp - HS_TEST_DIR: /scratch/docker-build/vpp/test/hs-test - CCACHE_DIR: /scratch/ccache/${{ matrix.os }}-${{ matrix.executor_arch }} - MAKE_PARALLEL_JOBS: 16 - TEST_JOBS: "auto" - TUI_LINE: "*******************************************************************" - - steps: - - name: Actions Checkout - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v6.0.2 - - - name: Setup Environment - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/setup-executor-env@b728d1589228e2891a4241a1bba362d76895c225 - - - name: Set Timestamp and Log Path - run: | - set -euxo pipefail - ts="$(date -u +%Y_%m_%d_%H%M%S_UTC)" - LOG_NAME="gha-vpp-periodic/vpp-verify-hst-master-${ts}-${{ matrix.build_type }}-${{ matrix.class }}-${{ matrix.os }}-${{ env.FDIO_NAMESPACE }}-${{ matrix.executor_arch }}" - echo "LOG_NAME=$LOG_NAME" >> "$GITHUB_ENV" - LOG_DIR="$WORKSPACE/logs/$LOG_NAME" - echo "LOG_DIR=$LOG_DIR" >> "$GITHUB_ENV" - mkdir -p "$LOG_DIR" - - - name: Show Environment - run: | - set -euo pipefail - env | sort | tee "$LOG_DIR/environment.log" - - - name: Show Nomad Dockerfile - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/show-nomad-dockerfile@b728d1589228e2891a4241a1bba362d76895c225 - - - name: Pull VPP master HEAD - run: | - set -euxo pipefail - cd "$WORKSPACE" - git checkout master - git pull - git show --stat - - - name: Install VPP dependencies - # yamllint disable-line rule:line-length - uses: fdio/vpp/.github/actions/vpp-install-deps@777792ecef2d5c93ce15fa5b7fdca999f5412c9d - with: - LOG_DIR: ${{ env.LOG_DIR }} - WORKSPACE: ${{ env.WORKSPACE }} - TUI_LINE: ${{ env.TUI_LINE }} - - - name: Install VPP external dependencies - # yamllint disable-line rule:line-length - uses: fdio/vpp/.github/actions/vpp-install-ext-deps@d0577011fb512d79e6914b6f1f11e2be4c09bc1e - with: - LOG_DIR: ${{ env.LOG_DIR }} - WORKSPACE: ${{ env.WORKSPACE }} - TUI_LINE: ${{ env.TUI_LINE }} - - - name: HST VPP Build - # yamllint disable-line rule:line-length - uses: fdio/vpp/.github/actions/vpp-build@3c0b690a135be65922fec705983478351a1b80b6 - with: - LOG_DIR: ${{ env.LOG_DIR }} - WORKSPACE: ${{ env.WORKSPACE }} - MAKE_PARALLEL_JOBS: ${{ env.MAKE_PARALLEL_JOBS }} - BUILD_TYPE: ${{ matrix['build_type'] }} - BUILD_HST: 'true' - TUI_LINE: ${{ env.TUI_LINE }} - - - name: Start Docker Daemon - uses: fdio/vpp/.github/actions/vpp-start-docker@f9e0f384d665800c8e580da853e26558dd6d2668 - with: - START_DOCKER_SCRIPT: start-docker.sh - TUI_LINE: ${{ env.TUI_LINE }} - - - name: HST Test VPP - if: success() - uses: fdio/vpp/.github/actions/vpp-hst-test@e970dcebe57999d0c04c63a466bb95604fcf9b4d - timeout-minutes: 90 - with: - LOG_DIR: ${{ env.LOG_DIR }} - WORKSPACE: ${{ env.WORKSPACE }} - BUILD_TYPE: ${{ matrix['build_type'] }} - HS_TEST_DIR: ${{ env.HS_TEST_DIR }} - TEST_JOBS: ${{ env.TEST_JOBS }} - TUI_LINE: ${{ env.TUI_LINE }} - - - name: AWS S3 Publish Logs - if: always() - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/aws-s3-publish-logs@1b8eff3ffd9d355b28052be9ad22acc57b3430b5 - with: - aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws_region: ${{ vars.AWS_REGION }} - s3_bucket: ${{ vars.S3_BUCKET }} - s3_path: ${{ env.LOG_NAME }} - archives_path: ${{ env.LOG_DIR }} - - - name: Check Archives Directory - id: check-archives - if: always() - run: | - set -euxo pipefail - archives_dir="$WORKSPACE/archives" - if [[ -d "$archives_dir" ]] && find "$archives_dir" -type f -print -quit | grep -q . ; then - echo "has_archives=true" >> "$GITHUB_OUTPUT" - else - echo "has_archives=false" >> "$GITHUB_OUTPUT" - fi - - - name: AWS S3 Publish Archives - if: ${{ always() && steps.check-archives.outputs.has_archives == 'true' }} - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/aws-s3-publish-logs@1b8eff3ffd9d355b28052be9ad22acc57b3430b5 - with: - aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws_region: ${{ vars.AWS_REGION }} - s3_bucket: ${{ vars.S3_BUCKET }} - s3_path: ${{ env.LOG_NAME }} - archives_path: ${{ env.WORKSPACE }}/archives diff --git a/.github/workflows/pr-verify.yml b/.github/workflows/pr-verify.yml new file mode 100644 index 000000000000..9d759a509a1f --- /dev/null +++ b/.github/workflows/pr-verify.yml @@ -0,0 +1,257 @@ +name: Verify pull request + +# yamllint disable-line rule:truthy +on: + pull_request: + branches: [master] + types: [opened, synchronize, reopened] + workflow_dispatch: + +# One run per ref: a new push to the same branch cancels the previous run. +concurrency: + group: pr-verify-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + style: + name: style + runs-on: ubuntu-24.04 + container: + image: debian:trixie + env: + # extras/scripts/checkstyle.sh picks clang-format-${CLANG_FORMAT_VER}; + # the Makefile sets 19 for Debian 13, and the same value has to reach the + # script when it is invoked directly. + CLANG_FORMAT_VER: "19" + steps: + - name: Disable APT cache cleanup + run: rm -f /etc/apt/apt.conf.d/docker-clean + + - name: Install base tools + run: | + apt-get update -qq + apt-get install -y -qq sudo make git + + - name: Check out the pull request + uses: actions/checkout@v4 + with: + # The coding style and API checks diff against a parent commit, and + # src/scripts/version reads the v* tags, so the whole history and + # tags are needed. + fetch-depth: 0 + fetch-tags: true + + - name: Install VPP checkstyle dependencies + run: | + git config --global --add safe.directory "$PWD" + # crcchecker.py refuses to run when a tracked file is modified, so + # this step must not rewrite the Makefile. + make UNATTENDED=yes install-dep + + - name: Check the feature list + run: make checkfeaturelist + + - name: Check the C coding style + # A pull_request checkout is the merge commit, whose first parent is + # the base branch tip, so HEAD~1..HEAD is the whole pull request. + run: make checkstyle + + - name: Check the API for incompatible changes + # crcchecker.py compares HEAD~1..HEAD as well, and refuses to run when + # the working tree is dirty, so this runs before the Python check + # builds anything. + run: make checkstyle-api + + - name: Check the Python coding style + run: make checkstyle-python + + - name: Check the commit messages of the pull request + run: | + set -eu + base="${{ github.event.pull_request.base.sha }}" + head="${{ github.event.pull_request.head.sha }}" + if [ -z "${base}" ] || [ -z "${head}" ]; then + make checkstyle-commit + exit 0 + fi + checked_out=$(git rev-parse HEAD) + trap 'git checkout --quiet "${checked_out}"' EXIT + for commit in $(git rev-list --reverse "${base}..${head}"); do + echo "Checking commit message of ${commit}" + git checkout --quiet "${commit}" + extras/scripts/check_commit_msg.sh + done + + build-test: + name: build and test + runs-on: ubuntu-24.04 + container: + image: debian:trixie + # The test framework reserves SHM_PER_PROCESS per worker on top of a + # 1 GiB minimum; the 64 MiB container default is far too small. + options: --shm-size=2g + env: + CCACHE_COMPRESS: "true" + CCACHE_MAXSIZE: 2G + steps: + - name: Disable APT cache cleanup + run: rm -f /etc/apt/apt.conf.d/docker-clean + + - name: Install base tools + run: | + apt-get update -qq + apt-get install -y -qq sudo make git + + - name: Check out the pull request + uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true + + # make install-dep installs $(DEB_DEPENDS), which is defined in the + # Makefile, so key the apt archive cache on that file: the key moves when + # the package list moves, and the run that first needs a new package is + # the run that stores it. The install step rewrites the Makefile + # afterwards, so the hash is taken into an output before that and both + # cache steps read the same value. + - name: Hash the dependency list + id: deps + run: | + git config --global --add safe.directory "$PWD" + echo "hash=$(git hash-object Makefile)" >> "$GITHUB_OUTPUT" + + - name: Restore apt archive cache + id: apt-cache + uses: actions/cache/restore@v4 + with: + path: /var/cache/apt/archives + key: apt-debian13-${{ runner.arch }}-${{ github.job }}-${{ steps.deps.outputs.hash }} + restore-keys: | + apt-debian13-${{ runner.arch }}-${{ github.job }}- + + - name: Install VPP build dependencies + run: | + sed -i '/^\s*git config commit\.template/s/^\(\s*\)/\1-/' Makefile + make UNATTENDED=yes install-dep + + # Saving is skipped when the exact key was restored: that entry already + # holds what this run would store, and asking to save a key that exists + # fails the step with "Unable to reserve cache". + - name: Save apt archive cache + if: steps.apt-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + path: /var/cache/apt/archives + key: apt-debian13-${{ runner.arch }}-${{ github.job }}-${{ steps.deps.outputs.hash }} + + # Keyed by commit rather than by ref alone: a fixed key lets only the + # first build of a ref store it, and every later build is asked to save a + # key that already exists. A commit key gives each build its own entry, + # and the prefix in restore-keys still lends a run the newest entry of + # another commit on the same ref. + - name: Restore ccache + id: ccache + uses: actions/cache/restore@v4 + with: + path: build-root/.ccache + key: ccache-debug-debian13-${{ runner.arch }}-${{ github.ref_name }}-${{ github.sha }} + restore-keys: | + ccache-debug-debian13-${{ runner.arch }}-${{ github.ref_name }}- + ccache-debug-debian13-${{ runner.arch }}- + + # src/scripts/version calls git describe without --tags, and origin + # (sunoaki/vpp) has no v* tags, so the version is empty and the CMake + # configure aborts with "string sub-command REPLACE requires at least + # four arguments". Synthesise a version in the same format the DEB + # workflow uses, whose ordering is already thought through, whenever no + # tag is reachable. + - name: Provide a version when origin has no tag + run: | + if ! git describe --tags --long --match 'v[0-9]*' 2>/dev/null; then + echo "0.0.0-release+$(git log -1 --format=%ct).$(git rev-list --count HEAD)-g$(git rev-parse --short=7 HEAD)" \ + > src/scripts/.version + fi + + - name: Build VPP debug + run: | + git config --global --add safe.directory "$PWD" + export VPP_EXTRA_CMAKE_ARGS="\ + -DCMAKE_C_COMPILER_LAUNCHER=ccache \ + -DCMAKE_CXX_COMPILER_LAUNCHER=ccache" + ccache -z + make UNATTENDED=yes build + ccache -s + + - name: Save ccache + if: steps.ccache.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + path: build-root/.ccache + key: ccache-debug-debian13-${{ runner.arch }}-${{ github.ref_name }}-${{ github.sha }} + + # Turn the changed paths into a focused TEST filter. A src/plugins/ + # change selects when a test_*.py module exists; a + # src/vnet/ change selects only when test_.py exists + # exactly, because a prefix match on a directory such as ip would pull in + # every ip4/ip6/ipip/ipsec suite. Changes under src/vppinfra, src/svm and + # src/vlib select nothing here and use the fallback smoke suite. + - name: Derive a test filter from the changed paths + id: tests + run: | + set -eu + base="${{ github.event.pull_request.base.sha }}" + head="${{ github.event.pull_request.head.sha }}" + if [ -z "${base}" ] || [ -z "${head}" ]; then + base=$(git rev-parse HEAD^ 2>/dev/null || echo "") + head=$(git rev-parse HEAD) + fi + changed=$(git diff --name-only "${base}...${head}" 2>/dev/null || + git diff --name-only "${base}..${head}" 2>/dev/null || true) + suites="" + for path in ${changed}; do + case "${path}" in + src/plugins/*/*) + name=${path#src/plugins/} + name=${name%%/*} + name=${name//-/_} + compgen -G "test/test_${name}*.py" >/dev/null || continue + ;; + src/vnet/*/*) + name=${path#src/vnet/} + name=${name%%/*} + name=${name//-/_} + [ -f "test/test_${name}.py" ] || continue + ;; + *) + continue + ;; + esac + suites="${suites} ${name}" + done + suites=$(echo "${suites}" | tr ' ' '\n' | sed '/^$/d' | + sort -u | head -n 3 | tr '\n' ' ') + if [ -z "${suites}" ]; then + echo "No test derives from the changed paths; running the ip4 smoke suite" + suites="ip4" + fi + echo "Running TEST suites:${suites}" + echo "suites=${suites}" >> "$GITHUB_OUTPUT" + + - name: Remount /dev/shm for the test harness + uses: ./.github/actions/vpp-docker-runtime-setup + with: + WORKSPACE: ${{ github.workspace }} + SHM_SIZE: 2G + LOG_DIR: ${{ github.workspace }}/build-root/test-logs + + - name: Run the Python functional tests + run: | + git config --global --add safe.directory "$PWD" + set -eu + for suite in ${{ steps.tests.outputs.suites }}; do + echo "Running TEST=${suite}" + make UNATTENDED=yes TEST_JOBS=auto RETRIES=2 TEST="${suite}" test-debug + done diff --git a/.github/workflows/vpp-csit-verify-api.yml b/.github/workflows/vpp-csit-verify-api.yml deleted file mode 100644 index 4eaccf726b93..000000000000 --- a/.github/workflows/vpp-csit-verify-api.yml +++ /dev/null @@ -1,185 +0,0 @@ - -name: vpp-csit-verify-api - -on: - workflow_call: - inputs: - FDIO_NAMESPACE: - description: "Executor Namespace (prod|sandbox)" - required: true - type: string - default: prod - GERRIT_BRANCH: - description: "Branch that change is against" - required: true - type: string - GERRIT_CHANGE_URL: - description: "URL to the change" - required: true - type: string - GERRIT_CHANGE_NUMBER: - description: "The Gerrit number" - required: true - type: string - GERRIT_PATCHSET_NUMBER: - description: "The patch number for the change" - required: true - type: string - GERRIT_PROJECT: - description: "Project in Gerrit" - required: true - type: string - GERRIT_REFSPEC: - description: "Gerrit refspec of change" - required: true - type: string - LOG_PREFIX: - description: "Prefix of the log directory" - required: true - type: string - workflow_dispatch: - inputs: - FDIO_NAMESPACE: - description: "Executor Namespace (prod|sandbox)" - required: false - type: choice - options: - - sandbox - - prod - default: prod - GERRIT_BRANCH: - description: "Branch that change is against" - required: true - type: string - GERRIT_CHANGE_URL: - description: "URL to the change" - required: true - type: string - GERRIT_CHANGE_NUMBER: - description: "The Gerrit number" - required: true - type: string - GERRIT_PATCHSET_NUMBER: - description: "The patch number for the change" - required: true - type: string - GERRIT_PROJECT: - description: "Project in Gerrit" - required: true - type: string - GERRIT_REFSPEC: - description: "Gerrit refspec of change" - required: true - type: string - LOG_PREFIX: - description: "Prefix of the log directory" - required: true - type: string - -defaults: - run: - shell: bash - -permissions: - contents: read - actions: read - -jobs: - csit-verify-api: - name: ${{ matrix.build_type }}_${{ matrix.os }}_${{ matrix.executor_arch }} - runs-on: - - self-hosted - - nomad - - fdio:arch=${{ matrix.executor_arch }} - - fdio:class=${{ matrix.class }} - - fdio:size=${{ matrix.size }} - - fdio:namespace=${{ github.event.inputs.FDIO_NAMESPACE || 'prod' }} - - fdio:os=${{ matrix.os }} - - fdio:workflow=${{ github.run_id }} - - strategy: - fail-fast: false - matrix: - build_type: ['release'] - class: ['builder'] - os: ['ubuntu2404'] - executor_arch: ['x86_64'] - size: ['csit'] - - env: - WORKSPACE: /scratch/docker-build/vpp - CSIT_DIR: /scratch/docker-build/csit - GERRIT_BRANCH: ${{ inputs.GERRIT_BRANCH }} - GERRIT_CHANGE_URL: ${{ inputs.GERRIT_CHANGE_URL }} - GERRIT_PROJECT: ${{ inputs.GERRIT_PROJECT }} - GERRIT_REFSPEC: ${{ inputs.GERRIT_REFSPEC }} - GERRIT_CHANGE_NUMBER: ${{ inputs.GERRIT_CHANGE_NUMBER }} - GERRIT_PATCHSET_NUMBER: ${{ inputs.GERRIT_PATCHSET_NUMBER }} - LOG_PREFIX: ${{ inputs.LOG_PREFIX }} - FDIO_NAMESPACE: ${{ github.event.inputs.FDIO_NAMESPACE || 'prod' }} - LOG_NAME: "${{ inputs.LOG_PREFIX }}/verify-csit-api-${{ matrix.build_type }}-${{ matrix.class }}-${{ matrix.os }}-${{ github.event.inputs.FDIO_NAMESPACE || 'prod' }}-${{ matrix.executor_arch }}" - LOG_DIR: "" - TUI_LINE: "*******************************************************************" - - steps: - - name: Actions Checkout - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v6.0.2 - - - name: Setup Environment - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/setup-executor-env@b728d1589228e2891a4241a1bba362d76895c225 - - - name: Prepare Log Directory - run: | - set -euxo pipefail - echo "LOG_NAME=$LOG_NAME" >> "$GITHUB_ENV" - LOG_DIR="$WORKSPACE/logs/$LOG_NAME" - echo "LOG_DIR=$LOG_DIR" >> "$GITHUB_ENV" - mkdir -p "$LOG_DIR" - - - name: Show Environment - run: | - set -euo pipefail - env | sort | tee "$LOG_DIR/environment.log" - - - name: "Show Nomad Dockerfile" - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/show-nomad-dockerfile@b728d1589228e2891a4241a1bba362d76895c225 - - - name: Checkout VPP Gerrit Change - # yamllint disable-line rule:line-length - uses: fdio/vpp/.github/actions/vpp-checkout-gerrit-change@27fc2aa27f57c8f7055e8f1b6e5a9f41b0315873 - with: - WORKSPACE: ${{ env.WORKSPACE }} - GERRIT_BRANCH: ${{ env.GERRIT_BRANCH }} - GERRIT_REFSPEC: ${{ env.GERRIT_REFSPEC }} - TUI_LINE: ${{ env.TUI_LINE }} - - - name: Run CSIT check_crc.sh - run: | - set -euxo pipefail - { - # Checkout CSIT HEAD - cd "$CSIT_DIR" - git checkout HEAD - - # Run check_crc.sh from CSIT - cd "$WORKSPACE" - # TODO: fix csit bash scripts to not rely on CSIT_DIR being inside WORKSPACE - NEW_CSIT_DIR="$WORKSPACE/$(basename "$CSIT_DIR")" - mv "$CSIT_DIR" "$NEW_CSIT_DIR" - csit_entry_dir="$NEW_CSIT_DIR/resources/libraries/bash/entry" - source "${csit_entry_dir}/with_oper_for_vpp.sh" "check_crc.sh" - } | tee "$LOG_DIR/csit_api.log" - - - name: AWS S3 Publish Logs - if: always() - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/aws-s3-publish-logs@1b8eff3ffd9d355b28052be9ad22acc57b3430b5 - with: - aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws_region: ${{ vars.AWS_REGION }} - s3_bucket: ${{ vars.S3_BUCKET }} - s3_path: ${{ env.LOG_NAME }} - archives_path: ${{ env.LOG_DIR }} diff --git a/.github/workflows/vpp-merge-docs.yml b/.github/workflows/vpp-merge-docs.yml deleted file mode 100644 index ec5bf2b0b0c3..000000000000 --- a/.github/workflows/vpp-merge-docs.yml +++ /dev/null @@ -1,215 +0,0 @@ -name: vpp-merge-docs - -on: - workflow_call: - inputs: - FDIO_NAMESPACE: - description: "Executor Namespace (prod|sandbox)" - required: true - type: string - GERRIT_BRANCH: - description: "Branch that change is against" - required: true - type: string - GERRIT_CHANGE_ID: - description: "The ID for the change" - required: true - type: string - GERRIT_CHANGE_NUMBER: - description: "The Gerrit number" - required: true - type: string - GERRIT_CHANGE_URL: - description: "URL to the change" - required: true - type: string - GERRIT_EVENT_TYPE: - description: "Type of Gerrit event" - required: true - type: string - GERRIT_PATCHSET_NUMBER: - description: "The patch number for the change" - required: true - type: string - GERRIT_PATCHSET_REVISION: - description: "The revision sha" - required: true - type: string - GERRIT_PROJECT: - description: "Project in Gerrit" - required: true - type: string - GERRIT_REFSPEC: - description: "Gerrit refspec of change" - required: true - type: string - LOG_PREFIX: - description: "Prefix of the log directory" - required: true - type: string - workflow_dispatch: - inputs: - FDIO_NAMESPACE: - description: "Executor Namespace (prod|sandbox)" - required: false - type: choice - options: - - sandbox - - prod - default: prod - GERRIT_BRANCH: - description: "Branch that change is against" - required: true - type: string - GERRIT_CHANGE_ID: - description: "The ID for the change" - required: true - type: string - GERRIT_CHANGE_NUMBER: - description: "The Gerrit number" - required: true - type: string - GERRIT_CHANGE_URL: - description: "URL to the change" - required: true - type: string - GERRIT_EVENT_TYPE: - description: "Type of Gerrit event" - required: true - type: string - GERRIT_PATCHSET_NUMBER: - description: "The patch number for the change" - required: true - type: string - GERRIT_PATCHSET_REVISION: - description: "The revision sha" - required: true - type: string - GERRIT_PROJECT: - description: "Project in Gerrit" - required: true - type: string - GERRIT_REFSPEC: - description: "Gerrit refspec of change" - required: true - type: string - LOG_PREFIX: - description: "Prefix of the log directory" - required: true - type: string - -defaults: - run: - shell: bash - -permissions: - contents: read - actions: read - -jobs: - merge-docs: - name: ${{ matrix.build_type }}_${{ matrix.os }}_${{ matrix.executor_arch }} - runs-on: - - self-hosted - - nomad - - fdio:arch=${{ matrix.executor_arch }} - - fdio:class=${{ matrix.class }} - - fdio:namespace=${{ inputs.FDIO_NAMESPACE || 'prod' }} - - fdio:os=${{ matrix.os }} - - fdio:workflow=${{ github.run_id }} - - strategy: - fail-fast: false - matrix: - build_type: ['release'] - class: ['builder'] - os: ['ubuntu2404'] - executor_arch: ['x86_64'] - - env: - FDIO_NAMESPACE: ${{ inputs.FDIO_NAMESPACE }} - GERRIT_BRANCH: ${{ inputs.GERRIT_BRANCH }} - GERRIT_CHANGE_ID: ${{ inputs.GERRIT_CHANGE_ID }} - GERRIT_CHANGE_NUMBER: ${{ inputs.GERRIT_CHANGE_NUMBER }} - GERRIT_CHANGE_URL: ${{ inputs.GERRIT_CHANGE_URL }} - GERRIT_EVENT_TYPE: ${{ inputs.GERRIT_EVENT_TYPE }} - GERRIT_PATCHSET_NUMBER: ${{ inputs.GERRIT_PATCHSET_NUMBER }} - GERRIT_PATCHSET_REVISION: ${{ inputs.GERRIT_PATCHSET_REVISION }} - GERRIT_PROJECT: ${{ inputs.GERRIT_PROJECT }} - GERRIT_REFSPEC: ${{ inputs.GERRIT_REFSPEC }} - LOG_PREFIX: ${{ inputs.LOG_PREFIX }} - LOG_NAME: "${{ inputs.LOG_PREFIX }}/merge-docs-${{ matrix.build_type }}-${{ matrix.class }}-${{ matrix.os }}-${{ inputs.FDIO_NAMESPACE }}-${{ matrix.executor_arch }}" - LOG_DIR: "" - S3_DOCS_PATH: "" - S3_DOCS_URL: https://s3-docs.fd.io - WORKSPACE: /scratch/docker-build/vpp - TUI_LINE: "*******************************************************************" - - steps: - - name: Actions Checkout - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v6.0.2 - - - name: Setup Environment - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/setup-executor-env@b728d1589228e2891a4241a1bba362d76895c225 - - - name: Prepare Log Directory - run: | - set -euxo pipefail - echo "LOG_NAME=$LOG_NAME" >> "$GITHUB_ENV" - LOG_DIR="$WORKSPACE/logs/$LOG_NAME" - echo "LOG_DIR=$LOG_DIR" >> "$GITHUB_ENV" - mkdir -p "$LOG_DIR" - vpp_release="$(${WORKSPACE}/build-root/scripts/version rpm-version)" - echo "S3_DOCS_PATH=vpp/$vpp_release" >> "$GITHUB_ENV" - - name: Show Environment - run: | - set -euo pipefail - env | sort | tee "$LOG_DIR/environment.log" - - - name: Show Nomad Dockerfile - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/show-nomad-dockerfile@b728d1589228e2891a4241a1bba362d76895c225 - - - name: Checkout VPP Gerrit Change - # yamllint disable-line rule:line-length - uses: fdio/vpp/.github/actions/vpp-checkout-gerrit-change@27fc2aa27f57c8f7055e8f1b6e5a9f41b0315873 - with: - WORKSPACE: ${{ env.WORKSPACE }} - GERRIT_BRANCH: ${{ env.GERRIT_BRANCH }} - GERRIT_REFSPEC: ${{ env.GERRIT_REFSPEC }} - TUI_LINE: ${{ env.TUI_LINE }} - - - name: Make Docs - id: make-docs - run: | - set -euxo pipefail - cd "$WORKSPACE" - make docs | tee "$LOG_DIR/make_docs.log" - - - name: AWS S3 Publish Docs - if: ${{ steps.make-docs.outcome == 'success' && inputs.FDIO_NAMESPACE == 'prod' }} - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/aws-s3-publish-logs@1b8eff3ffd9d355b28052be9ad22acc57b3430b5 - with: - aws_access_key_id: ${{ secrets.AWS_DOCS_ACCESS_KEY_ID }} - aws_secret_access_key: ${{ secrets.AWS_DOCS_SECRET_ACCESS_KEY }} - aws_region: ${{ vars.AWS_REGION }} - s3_bucket: ${{ vars.S3_DOCS_BUCKET }} - s3_path: ${{ env.S3_DOCS_PATH }} - s3_url: ${{ env.S3_DOCS_URL }} - jenkins_hostname: "" - archives_path: "${{ env.WORKSPACE }}/build-root/docs/html" - compress_files: false - - - name: AWS S3 Publish Logs - if: always() - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/aws-s3-publish-logs@1b8eff3ffd9d355b28052be9ad22acc57b3430b5 - with: - aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws_region: ${{ vars.AWS_REGION }} - s3_bucket: ${{ vars.S3_BUCKET }} - s3_path: ${{ env.LOG_NAME }} - archives_path: ${{ env.LOG_DIR }} diff --git a/.github/workflows/vpp-merge-maketest.yml b/.github/workflows/vpp-merge-maketest.yml deleted file mode 100644 index 06fe376be6f5..000000000000 --- a/.github/workflows/vpp-merge-maketest.yml +++ /dev/null @@ -1,330 +0,0 @@ -name: vpp-merge-maketest - -on: - workflow_call: - inputs: - FDIO_NAMESPACE: - description: "Executor Namespace (prod|sandbox)" - required: true - type: string - GERRIT_BRANCH: - description: "Branch that change is against" - required: true - type: string - GERRIT_CHANGE_ID: - description: "The ID for the change" - required: true - type: string - GERRIT_CHANGE_NUMBER: - description: "The Gerrit number" - required: true - type: string - GERRIT_CHANGE_URL: - description: "URL to the change" - required: true - type: string - GERRIT_EVENT_TYPE: - description: "Type of Gerrit event" - required: true - type: string - GERRIT_PATCHSET_NUMBER: - description: "The patch number for the change" - required: true - type: string - GERRIT_PATCHSET_REVISION: - description: "The revision sha" - required: true - type: string - GERRIT_PROJECT: - description: "Project in Gerrit" - required: true - type: string - GERRIT_REFSPEC: - description: "Gerrit refspec of change" - required: true - type: string - LOG_PREFIX: - description: "Prefix of the log directory" - required: true - type: string - workflow_dispatch: - inputs: - FDIO_NAMESPACE: - description: "Executor Namespace (prod|sandbox)" - required: false - type: choice - options: - - sandbox - - prod - default: prod - GERRIT_BRANCH: - description: "Branch that change is against" - required: true - type: string - GERRIT_CHANGE_ID: - description: "The ID for the change" - required: true - type: string - GERRIT_CHANGE_NUMBER: - description: "The Gerrit number" - required: true - type: string - GERRIT_CHANGE_URL: - description: "URL to the change" - required: true - type: string - GERRIT_EVENT_TYPE: - description: "Type of Gerrit event" - required: true - type: string - GERRIT_PATCHSET_NUMBER: - description: "The patch number for the change" - required: true - type: string - GERRIT_PATCHSET_REVISION: - description: "The revision sha" - required: true - type: string - GERRIT_PROJECT: - description: "Project in Gerrit" - required: true - type: string - GERRIT_REFSPEC: - description: "Gerrit refspec of change" - required: true - type: string - LOG_PREFIX: - description: "Prefix of the log directory" - required: true - type: string - -defaults: - run: - shell: bash - -permissions: - contents: read - actions: read - -jobs: - merge-maketest: - name: ${{ matrix.build_type }}_${{ matrix.os }}_${{ matrix.executor_arch }} - runs-on: - - self-hosted - - nomad - - fdio:arch=${{ matrix.executor_arch }} - - fdio:class=builder - - fdio:namespace=${{ inputs.FDIO_NAMESPACE || 'prod' }} - - fdio:os=${{ matrix.os }} - - fdio:workflow=${{ github.run_id }} - - strategy: - fail-fast: false - matrix: - os: ['ubuntu2404', 'ubuntu2204', 'debian12'] - executor_arch: ['x86_64', 'aarch64'] - build_type: ['release'] - exclude: - # Exclude debian12 on aarch64 for all builds - - os: 'debian12' - executor_arch: 'aarch64' - - env: - FDIO_NAMESPACE: ${{ inputs.FDIO_NAMESPACE }} - GERRIT_BRANCH: ${{ inputs.GERRIT_BRANCH }} - GERRIT_CHANGE_ID: ${{ inputs.GERRIT_CHANGE_ID }} - GERRIT_CHANGE_NUMBER: ${{ inputs.GERRIT_CHANGE_NUMBER }} - GERRIT_CHANGE_URL: ${{ inputs.GERRIT_CHANGE_URL }} - GERRIT_EVENT_TYPE: ${{ inputs.GERRIT_EVENT_TYPE }} - GERRIT_PATCHSET_NUMBER: ${{ inputs.GERRIT_PATCHSET_NUMBER }} - GERRIT_PATCHSET_REVISION: ${{ inputs.GERRIT_PATCHSET_REVISION }} - GERRIT_PROJECT: ${{ inputs.GERRIT_PROJECT }} - GERRIT_REFSPEC: ${{ inputs.GERRIT_REFSPEC }} - LOG_PREFIX: ${{ inputs.LOG_PREFIX }} - LOG_NAME: "${{ inputs.LOG_PREFIX }}/merge-maketest-${{ matrix.build_type }}-${{ matrix.os }}-${{ inputs.FDIO_NAMESPACE }}-${{ matrix.executor_arch }}" - LOG_DIR: "" - WORKSPACE: /scratch/docker-build/vpp - CCACHE_DIR: /scratch/ccache/${{ matrix.os }}-${{ matrix.executor_arch }} - BUILD_TYPE: ${{ matrix.build_type }} - MAKE_PARALLEL_JOBS: 16 - TEST_JOBS: 4 - TEST_RETRIES: 3 - MAKE_TEST_OS: ${{ matrix.os == 'ubuntu2204' && 'ubuntu-22.04' || 'ubuntu-24.04' }} - MAKE_TEST_MULTIWORKER_OS: 'debian-12' - VPPAPIGEN_TEST_OS: ${{ matrix.os == 'ubuntu2204' && 'ubuntu-22.04' || 'ubuntu-24.04' }} - SHM_SIZE: ${{ matrix.executor_arch == 'aarch64' && '4096M' || '2048M' }} - TUI_LINE: "*******************************************************************" - VPP_TEARDOWN_TIMEOUT: 30 - - steps: - - name: Actions Checkout - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v6.0.2 - - - name: Setup Environment - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/setup-executor-env@b728d1589228e2891a4241a1bba362d76895c225 - - - name: Prepare Log Directory - run: | - set -euxo pipefail - echo "LOG_NAME=$LOG_NAME" >> "$GITHUB_ENV" - LOG_DIR="$WORKSPACE/logs/$LOG_NAME" - echo "LOG_DIR=$LOG_DIR" >> "$GITHUB_ENV" - mkdir -p "$LOG_DIR" - - - name: Show Environment - run: | - set -euo pipefail - env | sort | tee "$LOG_DIR/environment.log" - - - name: Show Nomad Dockerfile - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/show-nomad-dockerfile@b728d1589228e2891a4241a1bba362d76895c225 - - - name: Checkout VPP Gerrit Change - # yamllint disable-line rule:line-length - uses: fdio/vpp/.github/actions/vpp-checkout-gerrit-change@27fc2aa27f57c8f7055e8f1b6e5a9f41b0315873 - with: - WORKSPACE: ${{ env.WORKSPACE }} - GERRIT_BRANCH: ${{ env.GERRIT_BRANCH }} - GERRIT_REFSPEC: ${{ env.GERRIT_REFSPEC }} - TUI_LINE: ${{ env.TUI_LINE }} - - - name: Build VPP Packages - run: | - set -euxo pipefail - { - cd "$WORKSPACE" - # pkg-verify runs install-deps, install-ext-deps, and builds vpp packages - make UNATTENDED=yes pkg-verify - # force rebuild of vpp-opt-deps - apt-get remove -y vpp-opt-deps || true - make UNATTENDED=yes install-opt-deps || true - } | tee "$LOG_DIR/build-all-packages.log" - - - name: Push Packages to Packagecloud - if: ${{ always() && inputs.FDIO_NAMESPACE == 'prod' }} - run: | - set -euxo pipefail - pkgcloud_cmd="" - vpp_pkgs="" - - create_deb_push_cmds() - { - local distro="$(/usr/bin/facter operatingsystem)" - local codename="$(/usr/bin/facter lsbdistcodename)" - local downloads_dir="/root/Downloads" - local pkgcloud_acct="fdio" - - echo '{"url":"https://packagecloud.io","token":"${{ secrets.PACKAGECLOUD_TOKEN }}"}' > /root/.packagecloud - distro=${distro,,} # to lowercase - codename=${codename,,} # to lowercase - pkgcloud_cmd="package_cloud push ${pkgcloud_acct}/${GERRIT_BRANCH/stable\//}/${distro}/${codename}/main/ " - if [ "$distro" = "debian" ] || [ "$distro" = "ubuntu" ] ; then - vpp_pkgs="$(find . -type f -iname '*.deb' | xargs || true)" - if [ -z "$vpp_pkgs" ] ; then - echo -e "\n$TUI_LINE\n* ERROR: No .deb packages found to push\n* PACKAGECLOUD PUSH FAILED!\n$TUI_LINE\n" - exit 1 - fi - else - echo -e "\n$TUI_LINE\n* ERROR: Unknown distro: '$distro'\n* PACKAGECLOUD PUSH FAILED!\n$TUI_LINE\n" - exit 1 - fi - } - - if [ -f ~/.packagecloud ]; then - cd "$WORKSPACE" - create_deb_push_cmds - for pkg in $vpp_pkgs ; do - $pkgcloud_cmd $pkg || true - done - else - echo -e "\n$TUI_LINE\n* ERROR: Missing '~/.packagecloud' for user '$(id)'\n* PACKAGECLOUD PUSH FAILED!\n$TUI_LINE\n" - exit 1 - fi - echo -e "\n$TUI_LINE\n* PACKAGECLOUD PUSH COMPLETE\n$TUI_LINE\n" - - - name: Post Packagecloud Push - if: ${{ always() && inputs.FDIO_NAMESPACE == 'prod' }} - run: | - echo '{"url":"https://packagecloud.io","token":"$token"}' > /root/.packagecloud - - - name: Remove vpp-opt-deps Before Make Test - if: always() - run: | - set -euxo pipefail - apt-get remove -y vpp-opt-deps || true - - - name: Setup Docker VPP Runtime Environment for 'make test' - # yamllint disable-line rule:line-length - uses: fdio/vpp/.github/actions/vpp-docker-runtime-setup@777792ecef2d5c93ce15fa5b7fdca999f5412c9d - with: - LOG_DIR: ${{ env.LOG_DIR }} - WORKSPACE: ${{ env.WORKSPACE }} - SHM_SIZE: ${{ env.SHM_SIZE }} - TUI_LINE: ${{ env.TUI_LINE }} - - - name: VPP Make Test - # yamllint disable-line rule:line-length - uses: fdio/vpp/.github/actions/vpp-make-test@03f35503fee57df2348617498db28fde557a3880 - timeout-minutes: 120 - with: - LOG_DIR: ${{ env.LOG_DIR }} - WORKSPACE: ${{ env.WORKSPACE }} - BUILD_TYPE: ${{ matrix.build_type }} - TEST_JOBS: ${{ env.TEST_JOBS }} - TEST_RETRIES: ${{ env.TEST_RETRIES }} - MAKE_TEST_OS: ${{ env.MAKE_TEST_OS }} - MAKE_TEST_MULTIWORKER_OS: ${{ env.MAKE_TEST_MULTIWORKER_OS }} - VPPAPIGEN_TEST_OS: ${{ env.VPPAPIGEN_TEST_OS }} - TUI_LINE: ${{ env.TUI_LINE }} - - - name: AWS S3 Publish Logs - if: always() - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/aws-s3-publish-logs@1b8eff3ffd9d355b28052be9ad22acc57b3430b5 - with: - aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws_region: ${{ vars.AWS_REGION }} - s3_bucket: ${{ vars.S3_BUCKET }} - s3_path: ${{ env.LOG_NAME }} - archives_path: ${{ env.LOG_DIR }} - - - name: Check Archives Directory - id: check-archives - if: always() - run: | - set -euxo pipefail - archives_dir="$WORKSPACE/archives" - unittest_failed_dir="/tmp/vpp-failed-unittests" - for failed_test in $(ls "$unittest_failed_dir") - do - unittest_failed_test="$unittest_failed_dir"/"$failed_test" - if [ -f "$unittest_failed_test" ] ; then - cp -a "$unittest_failed_test" "$archives_dir" - else - failed_test_dir="$archives_dir"/"$failed_test" - if [ ! -e "$failed_test_dir" ] ; then - mkdir -p "$failed_test_dir" - cp -a "$unittest_failed_test"/* "$failed_test_dir" - fi - fi - done - if [[ -d "$archives_dir" ]] && find "$archives_dir" -type f -print -quit | grep -q . ; then - echo "has_archives=true" >> "$GITHUB_OUTPUT" - else - echo "has_archives=false" >> "$GITHUB_OUTPUT" - fi - - - name: AWS S3 Publish Archives - if: ${{ always() && steps.check-archives.outputs.has_archives == 'true' }} - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/aws-s3-publish-logs@1b8eff3ffd9d355b28052be9ad22acc57b3430b5 - with: - aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws_region: ${{ vars.AWS_REGION }} - s3_bucket: ${{ vars.S3_BUCKET }} - s3_path: ${{ env.LOG_NAME }} - archives_path: ${{ env.WORKSPACE }}/archives diff --git a/.github/workflows/vpp-verify-arm-drivers.yml b/.github/workflows/vpp-verify-arm-drivers.yml deleted file mode 100644 index 68644e4e883f..000000000000 --- a/.github/workflows/vpp-verify-arm-drivers.yml +++ /dev/null @@ -1,224 +0,0 @@ -name: vpp-verify-arm-drivers - -on: - workflow_call: - inputs: - FDIO_NAMESPACE: - description: "Executor Namespace (prod|sandbox)" - required: true - type: string - default: prod - GERRIT_BRANCH: - description: "Branch that change is against" - required: true - type: string - GERRIT_CHANGE_NUMBER: - description: "The Gerrit number" - required: true - type: string - GERRIT_PATCHSET_NUMBER: - description: "The patch number for the change" - required: true - type: string - GERRIT_CHANGE_URL: - description: "URL to the change" - required: true - type: string - GERRIT_PROJECT: - description: "Project in Gerrit" - required: true - type: string - GERRIT_REFSPEC: - description: "Gerrit refspec of change" - required: true - type: string - LOG_PREFIX: - description: "Prefix of the log directory" - required: true - type: string - workflow_dispatch: - inputs: - FDIO_NAMESPACE: - description: "Executor Namespace (prod|sandbox)" - required: false - type: choice - options: - - sandbox - - prod - default: prod - GERRIT_BRANCH: - description: "Branch that change is against" - required: true - type: string - GERRIT_CHANGE_ID: - description: "The ID for the change" - required: true - type: string - GERRIT_CHANGE_NUMBER: - description: "The Gerrit number" - required: true - type: string - GERRIT_CHANGE_URL: - description: "URL to the change" - required: true - type: string - GERRIT_EVENT_TYPE: - description: "Type of Gerrit event" - required: true - type: string - GERRIT_PATCHSET_NUMBER: - description: "The patch number for the change" - required: true - type: string - GERRIT_PATCHSET_REVISION: - description: "The revision sha" - required: true - type: string - GERRIT_PROJECT: - description: "Project in Gerrit" - required: true - type: string - GERRIT_REFSPEC: - description: "Gerrit refspec of change" - required: true - type: string - LOG_PREFIX: - description: "Prefix of the log directory" - required: true - type: string - -defaults: - run: - shell: bash - -permissions: - contents: read - actions: read - -jobs: - verify-arm-drivers: - name: ${{ matrix.build_type }}_${{ matrix.os }}_${{ matrix.executor_arch }} - runs-on: - - self-hosted - - nomad - - fdio:arch=${{ matrix.executor_arch }} - - fdio:class=${{ matrix.class }} - - fdio:namespace=${{ inputs.FDIO_NAMESPACE || 'prod' }} - - fdio:os=${{ matrix.os }} - - fdio:workflow=${{ github.run_id }} - - strategy: - fail-fast: false - matrix: - build_type: ['release'] - class: ['builder'] - os: ['ubuntu2404'] - executor_arch: ['aarch64'] - - env: - FDIO_NAMESPACE: ${{ inputs.FDIO_NAMESPACE }} - GERRIT_BRANCH: ${{ inputs.GERRIT_BRANCH }} - GERRIT_CHANGE_URL: ${{ inputs.GERRIT_CHANGE_URL }} - GERRIT_CHANGE_NUMBER: ${{ inputs.GERRIT_CHANGE_NUMBER }} - GERRIT_PATCHSET_NUMBER: ${{ inputs.GERRIT_PATCHSET_NUMBER }} - GERRIT_PROJECT: ${{ inputs.GERRIT_PROJECT }} - GERRIT_REFSPEC: ${{ inputs.GERRIT_REFSPEC }} - LOG_PREFIX: ${{ inputs.LOG_PREFIX }} - LOG_NAME: "${{ inputs.LOG_PREFIX }}/verify-arm-drivers-${{ matrix.build_type }}-${{ matrix.class }}-${{ matrix.os }}-${{ inputs.FDIO_NAMESPACE }}-${{ matrix.executor_arch }}" - LOG_DIR: "" - WORKSPACE: /scratch/docker-build/vpp - CCACHE_DIR: /scratch/ccache/${{ matrix.os }}-${{ matrix.executor_arch }} - TUI_LINE: "*******************************************************************" - - steps: - - name: Actions Checkout - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v6.0.2 - - - name: Setup Environment - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/setup-executor-env@b728d1589228e2891a4241a1bba362d76895c225 - - - name: Prepare Log Directory - run: | - set -euxo pipefail - echo "LOG_NAME=$LOG_NAME" >> "$GITHUB_ENV" - LOG_DIR="$WORKSPACE/logs/$LOG_NAME" - echo "LOG_DIR=$LOG_DIR" >> "$GITHUB_ENV" - mkdir -p "$LOG_DIR" - - - name: Show Environment - run: | - set -euo pipefail - ls -l "$LOG_DIR" - env | sort | tee "$LOG_DIR/environment.log" - - - name: Show Nomad Dockerfile - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/show-nomad-dockerfile@b728d1589228e2891a4241a1bba362d76895c225 - - - name: Checkout VPP Gerrit Change - # yamllint disable-line rule:line-length - uses: fdio/vpp/.github/actions/vpp-checkout-gerrit-change@27fc2aa27f57c8f7055e8f1b6e5a9f41b0315873 - with: - WORKSPACE: ${{ env.WORKSPACE }} - GERRIT_BRANCH: ${{ env.GERRIT_BRANCH }} - GERRIT_REFSPEC: ${{ env.GERRIT_REFSPEC }} - TUI_LINE: ${{ env.TUI_LINE }} - - - name: Install VPP dependencies - uses: fdio/vpp/.github/actions/vpp-install-deps@777792ecef2d5c93ce15fa5b7fdca999f5412c9d - with: - LOG_DIR: ${{ env.LOG_DIR }} - WORKSPACE: ${{ env.WORKSPACE }} - TUI_LINE: ${{ env.TUI_LINE }} - - - name: Install VPP external dependencies - uses: fdio/vpp/.github/actions/vpp-install-ext-deps@d0577011fb512d79e6914b6f1f11e2be4c09bc1e - with: - LOG_DIR: ${{ env.LOG_DIR }} - WORKSPACE: ${{ env.WORKSPACE }} - TUI_LINE: ${{ env.TUI_LINE }} - - - name: Build VPP ARM Drivers - run: | - set -euxo pipefail - cd "$WORKSPACE" - ls -l "$LOG_DIR" - BUILD_RESULT="* SUCCESSFULLY COMPLETED" - BUILD_ERROR="" - RETVAL="0" - make_pkg_verify_arm_driver() { - vpp_platform="$1" - git clean -fdx build build-root src test || true - if ! make UNATTENDED=yes pkg-verify VPP_PLATFORM="$vpp_platform" ; then - BUILD_ERROR+="* FAILED 'make UNATTENDED=yes pkg-verify VPP_PLATFORM=$vpp_platform'\n" - fi - } - if [ -f $WORKSPACE/extras/scripts/build_static_vppctl.sh ] ; then - if ! $WORKSPACE/extras/scripts/build_static_vppctl.sh ; then - BUILD_ERROR="* FAILED '$WORKSPACE/extras/scripts/build_static_vppctl.sh'\n" - fi - fi - make_pkg_verify_arm_driver cn913x | tee "$LOG_DIR/arm_driver_build_cn913x.log" - make_pkg_verify_arm_driver octeon9 | tee "$LOG_DIR/arm_driver_build_octeon9.log" - make_pkg_verify_arm_driver octeon10 | tee "$LOG_DIR/arm_driver_build_octeon10.log" - { - if [ -n "$BUILD_ERROR" ] ; then - BUILD_RESULT="$BUILD_ERROR" - RETVAL="1" - fi - echo -e "\n$TUI_LINE\n* VPP ${OS_ID^^}-${OS_VERSION_ID}-${OS_ARCH^^} ARM DRIVERS BUILD\n$BUILD_RESULT\n$TUI_LINE\n" - } | tee "$LOG_DIR/arm_driver_build_results.log" - exit "$RETVAL" - - - name: AWS S3 Publish Logs - if: always() - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/aws-s3-publish-logs@1b8eff3ffd9d355b28052be9ad22acc57b3430b5 - with: - aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws_region: ${{ vars.AWS_REGION }} - s3_bucket: ${{ vars.S3_BUCKET }} - s3_path: ${{ env.LOG_NAME }} - archives_path: ${{ env.LOG_DIR }} diff --git a/.github/workflows/vpp-verify-checkstyle.yml b/.github/workflows/vpp-verify-checkstyle.yml deleted file mode 100644 index 6bd6ecd8bda2..000000000000 --- a/.github/workflows/vpp-verify-checkstyle.yml +++ /dev/null @@ -1,266 +0,0 @@ -name: vpp-verify-checkstyle - -# yamllint disable-line rule:truthy -on: - workflow_call: - inputs: - FDIO_NAMESPACE: - description: "Executor Namespace (prod|sandbox)" - required: true - type: string - GERRIT_BRANCH: - description: "Branch that change is against" - required: true - type: string - GERRIT_CHANGE_ID: - description: "The ID for the change" - required: true - type: string - GERRIT_CHANGE_NUMBER: - description: "The Gerrit number" - required: true - type: string - GERRIT_CHANGE_URL: - description: "URL to the change" - required: true - type: string - GERRIT_EVENT_TYPE: - description: "Type of Gerrit event" - required: true - type: string - GERRIT_PATCHSET_NUMBER: - description: "The patch number for the change" - required: true - type: string - GERRIT_PATCHSET_REVISION: - description: "The revision sha" - required: true - type: string - GERRIT_PROJECT: - description: "Project in Gerrit" - required: true - type: string - GERRIT_REFSPEC: - description: "Gerrit refspec of change" - required: true - type: string - LOG_PREFIX: - description: "Prefix of the log directory" - required: true - type: string - workflow_dispatch: - inputs: - FDIO_NAMESPACE: - description: "Executor Namespace (prod|sandbox)" - required: false - type: choice - options: - - sandbox - - prod - default: prod - GERRIT_BRANCH: - description: "Branch that change is against" - required: true - type: string - GERRIT_CHANGE_ID: - description: "The ID for the change" - required: true - type: string - GERRIT_CHANGE_NUMBER: - description: "The Gerrit number" - required: true - type: string - GERRIT_CHANGE_URL: - description: "URL to the change" - required: true - type: string - GERRIT_EVENT_TYPE: - description: "Type of Gerrit event" - required: true - type: string - GERRIT_PATCHSET_NUMBER: - description: "The patch number for the change" - required: true - type: string - GERRIT_PATCHSET_REVISION: - description: "The revision sha" - required: true - type: string - GERRIT_PROJECT: - description: "Project in Gerrit" - required: true - type: string - GERRIT_REFSPEC: - description: "Gerrit refspec of change" - required: true - type: string - LOG_PREFIX: - description: "Prefix of the log directory" - required: true - type: string - -defaults: - run: - shell: bash - -permissions: - contents: read - actions: read - -jobs: - verify-checkstyle: - name: ${{ matrix.build_type }}_${{ matrix.os }}_${{ matrix.executor_arch }} - runs-on: - - self-hosted - - nomad - - fdio:arch=${{ matrix.executor_arch }} - - fdio:class=${{ matrix.class }} - - fdio:namespace=${{ inputs.FDIO_NAMESPACE || 'prod' }} - - fdio:os=${{ matrix.os }} - - fdio:workflow=${{ github.run_id }} - - strategy: - fail-fast: false - matrix: - build_type: ['release'] - class: ['builder'] - os: ['ubuntu2404'] - executor_arch: ['x86_64'] - - env: - FDIO_NAMESPACE: ${{ inputs.FDIO_NAMESPACE }} - GERRIT_BRANCH: ${{ inputs.GERRIT_BRANCH }} - GERRIT_CHANGE_ID: ${{ inputs.GERRIT_CHANGE_ID }} - GERRIT_CHANGE_NUMBER: ${{ inputs.GERRIT_CHANGE_NUMBER }} - GERRIT_CHANGE_URL: ${{ inputs.GERRIT_CHANGE_URL }} - GERRIT_EVENT_TYPE: ${{ inputs.GERRIT_EVENT_TYPE }} - GERRIT_PATCHSET_NUMBER: ${{ inputs.GERRIT_PATCHSET_NUMBER }} - GERRIT_PATCHSET_REVISION: ${{ inputs.GERRIT_PATCHSET_REVISION }} - GERRIT_PROJECT: ${{ inputs.GERRIT_PROJECT }} - GERRIT_REFSPEC: ${{ inputs.GERRIT_REFSPEC }} - LOG_PREFIX: ${{ inputs.LOG_PREFIX }} - WORKSPACE: /scratch/docker-build/vpp - LOG_NAME: "${{ inputs.LOG_PREFIX }}/verify-checkstyle-${{ matrix.build_type }}-${{ matrix.class }}-${{ matrix.os }}-${{ inputs.FDIO_NAMESPACE }}-${{ matrix.executor_arch }}" - LOG_DIR: "" - TUI_LINE: "*******************************************************************" - - steps: - - name: Setup Environment - id: setup-executor-env - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/setup-executor-env@b728d1589228e2891a4241a1bba362d76895c225 - - - name: Prepare Log Directory - run: | - set -euxo pipefail - echo "LOG_NAME=$LOG_NAME" >> "$GITHUB_ENV" - LOG_DIR="$WORKSPACE/logs/$LOG_NAME" - echo "LOG_DIR=$LOG_DIR" >> "$GITHUB_ENV" - mkdir -p "$LOG_DIR" - - - name: Show Environment - run: | - set -euo pipefail - env | sort | tee "$LOG_DIR/environment.log" - - - name: "Show Nomad Dockerfile" - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/show-nomad-dockerfile@b728d1589228e2891a4241a1bba362d76895c225 - - - name: Checkout VPP Gerrit Change - # yamllint disable-line rule:line-length - uses: fdio/vpp/.github/actions/vpp-checkout-gerrit-change@27fc2aa27f57c8f7055e8f1b6e5a9f41b0315873 - with: - WORKSPACE: ${{ env.WORKSPACE }} - GERRIT_BRANCH: ${{ env.GERRIT_BRANCH }} - GERRIT_REFSPEC: ${{ env.GERRIT_REFSPEC }} - TUI_LINE: ${{ env.TUI_LINE }} - - - name: Install VPP dependencies - # yamllint disable-line rule:line-length - uses: fdio/vpp/.github/actions/vpp-install-deps@777792ecef2d5c93ce15fa5b7fdca999f5412c9d - with: - LOG_DIR: ${{ env.LOG_DIR }} - WORKSPACE: ${{ env.WORKSPACE }} - TUI_LINE: ${{ env.TUI_LINE }} - - - name: Run Checkstyle Scripts - run: | - set -euxo pipefail - cd $WORKSPACE - { - BUILD_EXT_DIR="build/external" - MATRIX_FILE="$BUILD_EXT_DIR/mlx_rdma_dpdk_matrix.txt" - PKGS_DIR="$BUILD_EXT_DIR/packages" - - if git show --stat | grep -q "$MATRIX_FILE" ; then - RDMA_CORE_VERSION="$(grep rdma-core_version $PKGS_DIR/rdma-core.mk | grep -v '(' | mawk '{print $3}')" - DPDK_VERSION="$(grep dpdk_version $PKGS_DIR/dpdk.mk | grep -v '(' | mawk '{print $3}')" - CURRENT_MATRIX="rdma=$RDMA_CORE_VERSION dpdk=$DPDK_VERSION" - if grep -q "$CURRENT_MATRIX" "$MATRIX_FILE"; then - echo -e "$TUI_LINE\n* DPDK/RDMA-CORE matrix file update successfully verified\n$TUI_LINE" - else - echo -e "$TUI_LINE\n* ERROR: 'rdma=$RDMA_CORE_VERSION dpdk=$DPDK_VERSION' not found in $MATRIX_FILE!\n$TUI_LINE" - exit 1 - fi - else - echo -e "$TUI_LINE\n* DPDK/RDMA-CORE matrix file not modified in $GERRIT_CHANGE_URL \n$TUI_LINE" - fi - } | tee $LOG_DIR/dpdk_rdma_matrix_check.log - - make UNATTENDED=yes checkstyle | tee $LOG_DIR/make_checkstyle.log - - - name: Run Test Checkstyle Scripts - run: | - set -euxo pipefail - cd $WORKSPACE - make test-checkstyle | tee $LOG_DIR/test_checkstyle.log - - - name: Run API Checkstyle Scripts - run: | - set -euxo pipefail - cd $WORKSPACE - { - VPP_CRC_CHECKER="extras/scripts/crcchecker.py" - VPP_CRC_CHECKER_CMD="$VPP_CRC_CHECKER --check-patchset" - - # API checker complains if the git repo is not clean. - # Help diagnosing those issues easier - git --no-pager diff - echo "Running $VPP_CRC_CHECKER_CMD" - if $VPP_CRC_CHECKER_CMD; then - echo "API check successful" - else - RET_CODE=$? - echo "API check failed: ret code $RET_CODE; please read https://wiki.fd.io/view/VPP/ApiChangeProcess and discuss with ayourtch@gmail.com if unsure how to proceed" - echo "::error file=.ci/scripts/vpp/crcchecker.py,line=1::API check FAILED for $GITHUB_REF. See run: https://github.com/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" - exit $RET_CODE - fi - } | tee $LOG_DIR/api_checkstyle.log - - - name: Run Commit Message Checkstyle - run: | - set -euxo pipefail - cd $WORKSPACE - { - echo "Running extras/scripts/check_commit_msg.sh" - extras/scripts/check_commit_msg.sh - } | tee $LOG_DIR/commit_checkstyle.log - - - name: Run Docs Spell - run: | - set -euxo pipefail - cd $WORKSPACE - make docs-spell | tee $LOG_DIR/docs_spelling_checkstyle.log - - - name: AWS S3 Publish Logs - if: ${{ always() }} - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/aws-s3-publish-logs@1b8eff3ffd9d355b28052be9ad22acc57b3430b5 - with: - aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws_region: ${{ vars.AWS_REGION }} - s3_bucket: ${{ vars.S3_BUCKET }} - s3_path: ${{ env.LOG_NAME }} - archives_path: ${{ env.LOG_DIR }} diff --git a/.github/workflows/vpp-verify-docs.yml b/.github/workflows/vpp-verify-docs.yml deleted file mode 100644 index a1a10f87c4ce..000000000000 --- a/.github/workflows/vpp-verify-docs.yml +++ /dev/null @@ -1,200 +0,0 @@ -name: vpp-verify-docs - -on: - workflow_call: - inputs: - FDIO_NAMESPACE: - description: "Executor Namespace (prod|sandbox)" - required: true - type: string - default: prod - GERRIT_BRANCH: - description: "Branch that change is against" - required: true - type: string - GERRIT_CHANGE_NUMBER: - description: "The Gerrit number" - required: true - type: string - GERRIT_PATCHSET_NUMBER: - description: "The patch number for the change" - required: true - type: string - GERRIT_CHANGE_URL: - description: "URL to the change" - required: true - type: string - GERRIT_PROJECT: - description: "Project in Gerrit" - required: true - type: string - GERRIT_REFSPEC: - description: "Gerrit refspec of change" - required: true - type: string - LOG_PREFIX: - description: "Prefix of the log directory" - required: true - type: string - workflow_dispatch: - inputs: - FDIO_NAMESPACE: - description: "Executor Namespace (prod|sandbox)" - required: false - type: choice - options: - - sandbox - - prod - default: prod - GERRIT_BRANCH: - description: "Branch that change is against" - required: true - type: string - GERRIT_CHANGE_ID: - description: "The ID for the change" - required: true - type: string - GERRIT_CHANGE_NUMBER: - description: "The Gerrit number" - required: true - type: string - GERRIT_CHANGE_URL: - description: "URL to the change" - required: true - type: string - GERRIT_EVENT_TYPE: - description: "Type of Gerrit event" - required: true - type: string - GERRIT_PATCHSET_NUMBER: - description: "The patch number for the change" - required: true - type: string - GERRIT_PATCHSET_REVISION: - description: "The revision sha" - required: true - type: string - GERRIT_PROJECT: - description: "Project in Gerrit" - required: true - type: string - GERRIT_REFSPEC: - description: "Gerrit refspec of change" - required: true - type: string - LOG_PREFIX: - description: "Prefix of the log directory" - required: true - type: string - -defaults: - run: - shell: bash - -permissions: - contents: read - actions: read - -jobs: - verify-docs: - # yamllint disable-line rule:line-length - name: ${{ inputs.GERRIT_CHANGE_NUMBER }}/${{ inputs.GERRIT_PATCHSET_NUMBER}}__${{ inputs.GERRIT_BRANCH }}__${{ inputs.FDIO_NAMESPACE }} - runs-on: - - self-hosted - - nomad - - fdio:arch=${{ matrix.executor_arch }} - - fdio:class=${{ matrix.class }} - - fdio:namespace=${{ inputs.FDIO_NAMESPACE || 'prod' }} - - fdio:os=${{ matrix.os }} - - fdio:workflow=${{ github.run_id }} - - strategy: - fail-fast: false - matrix: - class: ['builder'] - os: ['ubuntu2404'] - executor_arch: ['x86_64'] - - env: - FDIO_NAMESPACE: ${{ inputs.FDIO_NAMESPACE }} - GERRIT_BRANCH: ${{ inputs.GERRIT_BRANCH }} - GERRIT_CHANGE_URL: ${{ inputs.GERRIT_CHANGE_URL }} - GERRIT_CHANGE_NUMBER: ${{ inputs.GERRIT_CHANGE_NUMBER }} - GERRIT_PATCHSET_NUMBER: ${{ inputs.GERRIT_PATCHSET_NUMBER }} - GERRIT_PROJECT: ${{ inputs.GERRIT_PROJECT }} - GERRIT_REFSPEC: ${{ inputs.GERRIT_REFSPEC }} - LOG_PREFIX: ${{ inputs.LOG_PREFIX }} - LOG_NAME: "" - LOG_DIR: "" - S3_DOCS_7DAY_PATH: "" - S3_DOCS_7DAY_URL: https://s3-docs-7day.fd.io/vex-yul-rot-jenkins-1 - WORKSPACE: /scratch/docker-build/vpp - TUI_LINE: "*******************************************************************" - - steps: - - name: Actions Checkout - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v6.0.2 - - - name: Setup Environment - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/setup-executor-env@b728d1589228e2891a4241a1bba362d76895c225 - - - name: Prepare Log Directory - run: | - set -euxo pipefail - LOG_NAME="${{ env.LOG_PREFIX }}/verify-docs-${{ matrix.class }}-${{ matrix.os }}-${{ inputs.FDIO_NAMESPACE }}-${{ matrix.executor_arch }}" - echo "LOG_NAME=$LOG_NAME" >> "$GITHUB_ENV" - echo "S3_DOCS_7DAY_PATH=$LOG_NAME/docs" >> "$GITHUB_ENV" - LOG_DIR="$WORKSPACE/logs/$LOG_NAME" - echo "LOG_DIR=$LOG_DIR" >> "$GITHUB_ENV" - mkdir -p "$LOG_DIR" - - - name: Show Environment - run: | - set -euo pipefail - env | sort | tee "$LOG_DIR/environment.log" - - - name: Show Nomad Dockerfile - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/show-nomad-dockerfile@b728d1589228e2891a4241a1bba362d76895c225 - - - name: Checkout VPP Gerrit Change - uses: fdio/vpp/.github/actions/vpp-checkout-gerrit-change@27fc2aa27f57c8f7055e8f1b6e5a9f41b0315873 - with: - WORKSPACE: ${{ env.WORKSPACE }} - GERRIT_BRANCH: ${{ env.GERRIT_BRANCH }} - GERRIT_REFSPEC: ${{ env.GERRIT_REFSPEC }} - TUI_LINE: ${{ env.TUI_LINE }} - - - name: Make Docs - id: make-docs - run: | - set -euxo pipefail - cd "$WORKSPACE" - make docs | tee "$LOG_DIR/make_docs.log" - - - name: AWS S3 Publish Docs - if: ${{ steps.make-docs.outcome == 'success' }} - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/aws-s3-publish-logs@1b8eff3ffd9d355b28052be9ad22acc57b3430b5 - with: - aws_access_key_id: ${{ secrets.AWS_DOCS_7DAY_ACCESS_KEY_ID }} - aws_secret_access_key: ${{ secrets.AWS_DOCS_7DAY_SECRET_ACCESS_KEY }} - aws_region: ${{ vars.AWS_REGION }} - s3_bucket: ${{ vars.S3_DOCS_7DAY_BUCKET}} - s3_path: ${{ env.S3_DOCS_7DAY_PATH }} - s3_url: ${{ env.S3_DOCS_7DAY_URL }} - archives_path: "${{ env.WORKSPACE }}/build-root/docs/html" - compress_files: false - - - name: AWS S3 Publish Logs - if: always() - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/aws-s3-publish-logs@1b8eff3ffd9d355b28052be9ad22acc57b3430b5 - with: - aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws_region: ${{ vars.AWS_REGION }} - s3_bucket: ${{ vars.S3_BUCKET }} - s3_path: ${{ env.LOG_NAME }} - archives_path: ${{ env.LOG_DIR }} diff --git a/.github/workflows/vpp-verify-gcc.yml b/.github/workflows/vpp-verify-gcc.yml deleted file mode 100644 index 051689daca4e..000000000000 --- a/.github/workflows/vpp-verify-gcc.yml +++ /dev/null @@ -1,217 +0,0 @@ -name: vpp-verify-gcc - -on: - workflow_call: - inputs: - FDIO_NAMESPACE: - description: "Executor Namespace (prod|sandbox)" - required: true - type: string - default: prod - GERRIT_BRANCH: - description: "Branch that change is against" - required: true - type: string - GERRIT_CHANGE_ID: - description: "The ID for the change" - required: true - type: string - GERRIT_CHANGE_NUMBER: - description: "The Gerrit number" - required: true - type: string - GERRIT_CHANGE_URL: - description: "URL to the change" - required: true - type: string - GERRIT_EVENT_TYPE: - description: "Type of Gerrit event" - required: true - type: string - GERRIT_PATCHSET_NUMBER: - description: "The patch number for the change" - required: true - type: string - GERRIT_PATCHSET_REVISION: - description: "The revision sha" - required: true - type: string - GERRIT_PROJECT: - description: "Project in Gerrit" - required: true - type: string - GERRIT_REFSPEC: - description: "Gerrit refspec of change" - required: true - type: string - LOG_PREFIX: - description: "Prefix of the log directory" - required: true - type: string - workflow_dispatch: - inputs: - FDIO_NAMESPACE: - description: "Executor Namespace (prod|sandbox)" - required: false - type: choice - options: - - sandbox - - prod - default: prod - GERRIT_BRANCH: - description: "Branch that change is against" - required: true - type: string - GERRIT_CHANGE_ID: - description: "The ID for the change" - required: true - type: string - GERRIT_CHANGE_NUMBER: - description: "The Gerrit number" - required: true - type: string - GERRIT_CHANGE_URL: - description: "URL to the change" - required: true - type: string - GERRIT_EVENT_TYPE: - description: "Type of Gerrit event" - required: true - type: string - GERRIT_PATCHSET_NUMBER: - description: "The patch number for the change" - required: true - type: string - GERRIT_PATCHSET_REVISION: - description: "The revision sha" - required: true - type: string - GERRIT_PROJECT: - description: "Project in Gerrit" - required: true - type: string - GERRIT_REFSPEC: - description: "Gerrit refspec of change" - required: true - type: string - LOG_PREFIX: - description: "Prefix of the log directory" - required: true - type: string - -defaults: - run: - shell: bash - -permissions: - contents: read - actions: read - -jobs: - verify-gcc: - name: ${{ matrix.build_type }}_${{ matrix.os }}_${{ matrix.executor_arch }} - runs-on: - - self-hosted - - nomad - - fdio:arch=${{ matrix.executor_arch }} - - fdio:class=builder - - fdio:namespace=${{ inputs.FDIO_NAMESPACE || 'prod' }} - - fdio:os=${{ matrix.os }} - - fdio:workflow=${{ github.run_id }} - - strategy: - fail-fast: false - matrix: - os: ['ubuntu2404'] - executor_arch: ['x86_64'] - build_type: ['debug', 'release'] - - env: - FDIO_NAMESPACE: ${{ inputs.FDIO_NAMESPACE }} - GERRIT_BRANCH: ${{ inputs.GERRIT_BRANCH }} - GERRIT_CHANGE_URL: ${{ inputs.GERRIT_CHANGE_URL }} - GERRIT_CHANGE_NUMBER: ${{ inputs.GERRIT_CHANGE_NUMBER }} - GERRIT_PATCHSET_NUMBER: ${{ inputs.GERRIT_PATCHSET_NUMBER }} - GERRIT_PROJECT: ${{ inputs.GERRIT_PROJECT }} - GERRIT_REFSPEC: ${{ inputs.GERRIT_REFSPEC }} - LOG_PREFIX: ${{ inputs.LOG_PREFIX }} - LOG_NAME: "${{ inputs.LOG_PREFIX }}/verify-gcc-${{ matrix.build_type }}-${{ matrix.os }}-${{ inputs.FDIO_NAMESPACE }}-${{ matrix.executor_arch }}" - LOG_DIR: "" - WORKSPACE: /scratch/docker-build/vpp - CCACHE_DIR: /scratch/ccache/${{ matrix.os }}-${{ matrix.executor_arch }} - MAKE_PARALLEL_JOBS: 16 - TUI_LINE: "*******************************************************************" - VPP_TEARDOWN_TIMEOUT: 30 - - steps: - - name: Actions Checkout - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v6.0.2 - - - name: Setup Environment - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/setup-executor-env@b728d1589228e2891a4241a1bba362d76895c225 - - - name: Prepare Log Directory - run: | - set -euxo pipefail - echo "LOG_NAME=$LOG_NAME" >> "$GITHUB_ENV" - LOG_DIR="$WORKSPACE/logs/$LOG_NAME" - echo "LOG_DIR=$LOG_DIR" >> "$GITHUB_ENV" - mkdir -p "$LOG_DIR" - - - name: Show Environment - run: | - set -euo pipefail - env | sort | tee "$LOG_DIR/environment.log" - - - name: Show Nomad Dockerfile - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/show-nomad-dockerfile@b728d1589228e2891a4241a1bba362d76895c225 - - - name: Checkout VPP Gerrit Change - # yamllint disable-line rule:line-length - uses: fdio/vpp/.github/actions/vpp-checkout-gerrit-change@27fc2aa27f57c8f7055e8f1b6e5a9f41b0315873 - with: - WORKSPACE: ${{ env.WORKSPACE }} - GERRIT_BRANCH: ${{ env.GERRIT_BRANCH }} - GERRIT_REFSPEC: ${{ env.GERRIT_REFSPEC }} - TUI_LINE: ${{ env.TUI_LINE }} - - - name: Install VPP dependencies - # yamllint disable-line rule:line-length - uses: fdio/vpp/.github/actions/vpp-install-deps@777792ecef2d5c93ce15fa5b7fdca999f5412c9d - with: - LOG_DIR: ${{ env.LOG_DIR }} - WORKSPACE: ${{ env.WORKSPACE }} - TUI_LINE: ${{ env.TUI_LINE }} - - - name: Install VPP external dependencies - # yamllint disable-line rule:line-length - uses: fdio/vpp/.github/actions/vpp-install-ext-deps@d0577011fb512d79e6914b6f1f11e2be4c09bc1e - with: - LOG_DIR: ${{ env.LOG_DIR }} - WORKSPACE: ${{ env.WORKSPACE }} - TUI_LINE: ${{ env.TUI_LINE }} - - - name: Build VPP with GCC - # yamllint disable-line rule:line-length - uses: fdio/vpp/.github/actions/vpp-build@3c0b690a135be65922fec705983478351a1b80b6 - with: - LOG_DIR: ${{ env.LOG_DIR }} - WORKSPACE: ${{ env.WORKSPACE }} - MAKE_PARALLEL_JOBS: ${{ env.MAKE_PARALLEL_JOBS }} - BUILD_TYPE: ${{ matrix['build_type'] }} - USE_GCC: "true" - TUI_LINE: ${{ env.TUI_LINE }} - - - name: AWS S3 Publish Logs - if: always() - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/aws-s3-publish-logs@1b8eff3ffd9d355b28052be9ad22acc57b3430b5 - with: - aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws_region: ${{ vars.AWS_REGION }} - s3_bucket: ${{ vars.S3_BUCKET }} - s3_path: ${{ env.LOG_NAME }} - archives_path: ${{ env.LOG_DIR }} diff --git a/.github/workflows/vpp-verify-hst-u2204.yml b/.github/workflows/vpp-verify-hst-u2204.yml deleted file mode 100644 index bfb5479c07bf..000000000000 --- a/.github/workflows/vpp-verify-hst-u2204.yml +++ /dev/null @@ -1,252 +0,0 @@ -name: vpp-verify-hst-u2204 - -on: - workflow_call: - inputs: - FDIO_NAMESPACE: - description: "Executor Namespace (prod|sandbox)" - required: true - type: string - default: prod - GERRIT_BRANCH: - description: "Branch that change is against" - required: true - type: string - GERRIT_CHANGE_NUMBER: - description: "The Gerrit number" - required: true - type: string - GERRIT_PATCHSET_NUMBER: - description: "The patch number for the change" - required: true - type: string - GERRIT_CHANGE_URL: - description: "URL to the change" - required: true - type: string - GERRIT_PROJECT: - description: "Project in Gerrit" - required: true - type: string - GERRIT_REFSPEC: - description: "Gerrit refspec of change" - required: true - type: string - LOG_PREFIX: - description: "Prefix of the log directory" - required: true - type: string - workflow_dispatch: - inputs: - FDIO_NAMESPACE: - description: "Executor Namespace (prod|sandbox)" - required: false - type: choice - options: - - sandbox - - prod - default: prod - GERRIT_BRANCH: - description: "Branch that change is against" - required: true - type: string - GERRIT_CHANGE_ID: - description: "The ID for the change" - required: true - type: string - GERRIT_CHANGE_NUMBER: - description: "The Gerrit number" - required: true - type: string - GERRIT_CHANGE_URL: - description: "URL to the change" - required: true - type: string - GERRIT_EVENT_TYPE: - description: "Type of Gerrit event" - required: true - type: string - GERRIT_PATCHSET_NUMBER: - description: "The patch number for the change" - required: true - type: string - GERRIT_PATCHSET_REVISION: - description: "The revision sha" - required: true - type: string - GERRIT_PROJECT: - description: "Project in Gerrit" - required: true - type: string - GERRIT_REFSPEC: - description: "Gerrit refspec of change" - required: true - type: string - LOG_PREFIX: - description: "Prefix of the log directory" - required: true - type: string - -defaults: - run: - shell: bash - -permissions: - contents: read - actions: read - -jobs: - verify-hst: - name: ${{ matrix.build_type }}_${{ matrix.os }}_${{ matrix.executor_arch }} - runs-on: - - self-hosted - - nomad - - fdio:arch=${{ matrix.executor_arch }} - - fdio:class=${{ matrix.class }} - - fdio:size=${{ matrix.size }} - - fdio:namespace=${{ inputs.FDIO_NAMESPACE || 'prod' }} - - fdio:os=${{ matrix.os }} - - fdio:workflow=${{ github.run_id }} - - strategy: - fail-fast: false - matrix: - build_type: ['debug'] - class: ['hst'] - os: ['ubuntu2204'] - executor_arch: ['x86_64', 'aarch64'] - size: ['hst'] - - env: - FDIO_NAMESPACE: ${{ inputs.FDIO_NAMESPACE }} - LOG_PREFIX: ${{ inputs.LOG_PREFIX }} - LOG_NAME: "${{ inputs.LOG_PREFIX }}/verify-hst-${{ matrix.build_type }}-${{ matrix.class }}-${{ matrix.os }}-${{ inputs.FDIO_NAMESPACE }}-${{ matrix.executor_arch }}" - LOG_DIR: "" - GERRIT_BRANCH: ${{ inputs.GERRIT_BRANCH }} - GERRIT_CHANGE_NUMBER: ${{ inputs.GERRIT_CHANGE_NUMBER }} - GERRIT_PATCHSET_NUMBER: ${{ inputs.GERRIT_PATCHSET_NUMBER }} - GERRIT_CHANGE_URL: ${{ inputs.GERRIT_CHANGE_URL }} - GERRIT_PROJECT: ${{ inputs.GERRIT_PROJECT }} - GERRIT_REFSPEC: ${{ inputs.GERRIT_REFSPEC }} - WORKSPACE: /scratch/docker-build/vpp - HS_TEST_DIR: /scratch/docker-build/vpp/test/hs-test - CCACHE_DIR: /scratch/ccache/${{ matrix.os }}-${{ matrix.executor_arch }} - MAKE_PARALLEL_JOBS: 16 - TEST_JOBS: "auto" - TUI_LINE: "*******************************************************************" - - steps: - - name: Actions Checkout - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v6.0.2 - - - name: Setup Environment - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/setup-executor-env@b728d1589228e2891a4241a1bba362d76895c225 - - - name: Prepare Log Directory - run: | - set -euxo pipefail - echo "LOG_NAME=$LOG_NAME" >> "$GITHUB_ENV" - LOG_DIR="$WORKSPACE/logs/$LOG_NAME" - echo "LOG_DIR=$LOG_DIR" >> "$GITHUB_ENV" - mkdir -p "$LOG_DIR" - - - name: Show Environment - run: | - set -euo pipefail - env | sort | tee "$LOG_DIR/environment.log" - - - name: Show Nomad Dockerfile - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/show-nomad-dockerfile@b728d1589228e2891a4241a1bba362d76895c225 - - - name: Checkout VPP Gerrit Change - # yamllint disable-line rule:line-length - uses: fdio/vpp/.github/actions/vpp-checkout-gerrit-change@27fc2aa27f57c8f7055e8f1b6e5a9f41b0315873 - with: - LOG_DIR: ${{ env.LOG_DIR }} - WORKSPACE: ${{ env.WORKSPACE }} - GERRIT_BRANCH: ${{ env.GERRIT_BRANCH }} - GERRIT_REFSPEC: ${{ env.GERRIT_REFSPEC }} - TUI_LINE: ${{ env.TUI_LINE }} - - - name: Install VPP dependencies - # yamllint disable-line rule:line-length - uses: fdio/vpp/.github/actions/vpp-install-deps@777792ecef2d5c93ce15fa5b7fdca999f5412c9d - with: - LOG_DIR: ${{ env.LOG_DIR }} - WORKSPACE: ${{ env.WORKSPACE }} - TUI_LINE: ${{ env.TUI_LINE }} - - - name: Install VPP external dependencies - # yamllint disable-line rule:line-length - uses: fdio/vpp/.github/actions/vpp-install-ext-deps@d0577011fb512d79e6914b6f1f11e2be4c09bc1e - with: - LOG_DIR: ${{ env.LOG_DIR }} - WORKSPACE: ${{ env.WORKSPACE }} - TUI_LINE: ${{ env.TUI_LINE }} - - - name: HST VPP Build - # yamllint disable-line rule:line-length - uses: fdio/vpp/.github/actions/vpp-build@3c0b690a135be65922fec705983478351a1b80b6 - with: - LOG_DIR: ${{ env.LOG_DIR }} - WORKSPACE: ${{ env.WORKSPACE }} - MAKE_PARALLEL_JOBS: ${{ env.MAKE_PARALLEL_JOBS }} - BUILD_TYPE: ${{ matrix['build_type'] }} - BUILD_HST: 'true' - TUI_LINE: ${{ env.TUI_LINE }} - - - name: Start Docker Daemon - uses: fdio/vpp/.github/actions/vpp-start-docker@f9e0f384d665800c8e580da853e26558dd6d2668 - with: - START_DOCKER_SCRIPT: start-docker.sh - TUI_LINE: ${{ env.TUI_LINE }} - - - name: HST Test VPP - if: success() - uses: fdio/vpp/.github/actions/vpp-hst-test@e970dcebe57999d0c04c63a466bb95604fcf9b4d - timeout-minutes: 90 - with: - LOG_DIR: ${{ env.LOG_DIR }} - WORKSPACE: ${{ env.WORKSPACE }} - BUILD_TYPE: ${{ matrix['build_type'] }} - HS_TEST_DIR: ${{ env.HS_TEST_DIR }} - TEST_JOBS: ${{ env.TEST_JOBS }} - TUI_LINE: ${{ env.TUI_LINE }} - - - name: AWS S3 Publish Logs - if: always() - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/aws-s3-publish-logs@1b8eff3ffd9d355b28052be9ad22acc57b3430b5 - with: - aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws_region: ${{ vars.AWS_REGION }} - s3_bucket: ${{ vars.S3_BUCKET }} - s3_path: ${{ env.LOG_NAME }} - archives_path: ${{ env.LOG_DIR }} - - - name: Check Archives Directory - id: check-archives - if: always() - run: | - set -euxo pipefail - archives_dir="$WORKSPACE/archives" - if [[ -d "$archives_dir" ]] && find "$archives_dir" -type f -print -quit | grep -q . ; then - echo "has_archives=true" >> "$GITHUB_OUTPUT" - else - echo "has_archives=false" >> "$GITHUB_OUTPUT" - fi - - - name: AWS S3 Publish Archives - if: ${{ always() && steps.check-archives.outputs.has_archives == 'true' }} - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/aws-s3-publish-logs@1b8eff3ffd9d355b28052be9ad22acc57b3430b5 - with: - aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws_region: ${{ vars.AWS_REGION }} - s3_bucket: ${{ vars.S3_BUCKET }} - s3_path: ${{ env.LOG_NAME }} - archives_path: ${{ env.WORKSPACE }}/archives diff --git a/.github/workflows/vpp-verify-hst.yml b/.github/workflows/vpp-verify-hst.yml deleted file mode 100644 index f9ea8da08181..000000000000 --- a/.github/workflows/vpp-verify-hst.yml +++ /dev/null @@ -1,252 +0,0 @@ -name: vpp-verify-hst - -on: - workflow_call: - inputs: - FDIO_NAMESPACE: - description: "Executor Namespace (prod|sandbox)" - required: true - type: string - default: prod - GERRIT_BRANCH: - description: "Branch that change is against" - required: true - type: string - GERRIT_CHANGE_NUMBER: - description: "The Gerrit number" - required: true - type: string - GERRIT_PATCHSET_NUMBER: - description: "The patch number for the change" - required: true - type: string - GERRIT_CHANGE_URL: - description: "URL to the change" - required: true - type: string - GERRIT_PROJECT: - description: "Project in Gerrit" - required: true - type: string - GERRIT_REFSPEC: - description: "Gerrit refspec of change" - required: true - type: string - LOG_PREFIX: - description: "Prefix of the log directory" - required: true - type: string - workflow_dispatch: - inputs: - FDIO_NAMESPACE: - description: "Executor Namespace (prod|sandbox)" - required: false - type: choice - options: - - sandbox - - prod - default: prod - GERRIT_BRANCH: - description: "Branch that change is against" - required: true - type: string - GERRIT_CHANGE_ID: - description: "The ID for the change" - required: true - type: string - GERRIT_CHANGE_NUMBER: - description: "The Gerrit number" - required: true - type: string - GERRIT_CHANGE_URL: - description: "URL to the change" - required: true - type: string - GERRIT_EVENT_TYPE: - description: "Type of Gerrit event" - required: true - type: string - GERRIT_PATCHSET_NUMBER: - description: "The patch number for the change" - required: true - type: string - GERRIT_PATCHSET_REVISION: - description: "The revision sha" - required: true - type: string - GERRIT_PROJECT: - description: "Project in Gerrit" - required: true - type: string - GERRIT_REFSPEC: - description: "Gerrit refspec of change" - required: true - type: string - LOG_PREFIX: - description: "Prefix of the log directory" - required: true - type: string - -defaults: - run: - shell: bash - -permissions: - contents: read - actions: read - -jobs: - verify-hst: - name: ${{ matrix.build_type }}_${{ matrix.os }}_${{ matrix.executor_arch }} - runs-on: - - self-hosted - - nomad - - fdio:arch=${{ matrix.executor_arch }} - - fdio:class=${{ matrix.class }} - - fdio:size=${{ matrix.size }} - - fdio:namespace=${{ inputs.FDIO_NAMESPACE || 'prod' }} - - fdio:os=${{ matrix.os }} - - fdio:workflow=${{ github.run_id }} - - strategy: - fail-fast: false - matrix: - build_type: ['debug', 'release'] - class: ['hst'] - os: ['ubuntu2404'] - executor_arch: ['x86_64'] - size: ['hst'] - - env: - FDIO_NAMESPACE: ${{ inputs.FDIO_NAMESPACE }} - LOG_PREFIX: ${{ inputs.LOG_PREFIX }} - LOG_NAME: "${{ inputs.LOG_PREFIX }}/verify-hst-${{ matrix.build_type }}-${{ matrix.class }}-${{ matrix.os }}-${{ inputs.FDIO_NAMESPACE }}-${{ matrix.executor_arch }}" - LOG_DIR: "" - GERRIT_BRANCH: ${{ inputs.GERRIT_BRANCH }} - GERRIT_CHANGE_NUMBER: ${{ inputs.GERRIT_CHANGE_NUMBER }} - GERRIT_PATCHSET_NUMBER: ${{ inputs.GERRIT_PATCHSET_NUMBER }} - GERRIT_CHANGE_URL: ${{ inputs.GERRIT_CHANGE_URL }} - GERRIT_PROJECT: ${{ inputs.GERRIT_PROJECT }} - GERRIT_REFSPEC: ${{ inputs.GERRIT_REFSPEC }} - WORKSPACE: /scratch/docker-build/vpp - HS_TEST_DIR: /scratch/docker-build/vpp/test/hs-test - CCACHE_DIR: /scratch/ccache/${{ matrix.os }}-${{ matrix.executor_arch }} - MAKE_PARALLEL_JOBS: 16 - TEST_JOBS: "auto" - TUI_LINE: "*******************************************************************" - - steps: - - name: Actions Checkout - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v6.0.2 - - - name: Setup Environment - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/setup-executor-env@b728d1589228e2891a4241a1bba362d76895c225 - - - name: Prepare Log Directory - run: | - set -euxo pipefail - echo "LOG_NAME=$LOG_NAME" >> "$GITHUB_ENV" - LOG_DIR="$WORKSPACE/logs/$LOG_NAME" - echo "LOG_DIR=$LOG_DIR" >> "$GITHUB_ENV" - mkdir -p "$LOG_DIR" - - - name: Show Environment - run: | - set -euo pipefail - env | sort | tee "$LOG_DIR/environment.log" - - - name: Show Nomad Dockerfile - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/show-nomad-dockerfile@b728d1589228e2891a4241a1bba362d76895c225 - - - name: Checkout VPP Gerrit Change - # yamllint disable-line rule:line-length - uses: fdio/vpp/.github/actions/vpp-checkout-gerrit-change@27fc2aa27f57c8f7055e8f1b6e5a9f41b0315873 - with: - LOG_DIR: ${{ env.LOG_DIR }} - WORKSPACE: ${{ env.WORKSPACE }} - GERRIT_BRANCH: ${{ env.GERRIT_BRANCH }} - GERRIT_REFSPEC: ${{ env.GERRIT_REFSPEC }} - TUI_LINE: ${{ env.TUI_LINE }} - - - name: Install VPP dependencies - # yamllint disable-line rule:line-length - uses: fdio/vpp/.github/actions/vpp-install-deps@777792ecef2d5c93ce15fa5b7fdca999f5412c9d - with: - LOG_DIR: ${{ env.LOG_DIR }} - WORKSPACE: ${{ env.WORKSPACE }} - TUI_LINE: ${{ env.TUI_LINE }} - - - name: Install VPP external dependencies - # yamllint disable-line rule:line-length - uses: fdio/vpp/.github/actions/vpp-install-ext-deps@d0577011fb512d79e6914b6f1f11e2be4c09bc1e - with: - LOG_DIR: ${{ env.LOG_DIR }} - WORKSPACE: ${{ env.WORKSPACE }} - TUI_LINE: ${{ env.TUI_LINE }} - - - name: HST VPP Build - # yamllint disable-line rule:line-length - uses: fdio/vpp/.github/actions/vpp-build@3c0b690a135be65922fec705983478351a1b80b6 - with: - LOG_DIR: ${{ env.LOG_DIR }} - WORKSPACE: ${{ env.WORKSPACE }} - MAKE_PARALLEL_JOBS: ${{ env.MAKE_PARALLEL_JOBS }} - BUILD_TYPE: ${{ matrix['build_type'] }} - BUILD_HST: 'true' - TUI_LINE: ${{ env.TUI_LINE }} - - - name: Start Docker Daemon - uses: fdio/vpp/.github/actions/vpp-start-docker@f9e0f384d665800c8e580da853e26558dd6d2668 - with: - START_DOCKER_SCRIPT: start-docker.sh - TUI_LINE: ${{ env.TUI_LINE }} - - - name: HST Test VPP - if: success() - uses: fdio/vpp/.github/actions/vpp-hst-test@e970dcebe57999d0c04c63a466bb95604fcf9b4d - timeout-minutes: 90 - with: - LOG_DIR: ${{ env.LOG_DIR }} - WORKSPACE: ${{ env.WORKSPACE }} - BUILD_TYPE: ${{ matrix['build_type'] }} - HS_TEST_DIR: ${{ env.HS_TEST_DIR }} - TEST_JOBS: ${{ env.TEST_JOBS }} - TUI_LINE: ${{ env.TUI_LINE }} - - - name: AWS S3 Publish Logs - if: always() - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/aws-s3-publish-logs@1b8eff3ffd9d355b28052be9ad22acc57b3430b5 - with: - aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws_region: ${{ vars.AWS_REGION }} - s3_bucket: ${{ vars.S3_BUCKET }} - s3_path: ${{ env.LOG_NAME }} - archives_path: ${{ env.LOG_DIR }} - - - name: Check Archives Directory - id: check-archives - if: always() - run: | - set -euxo pipefail - archives_dir="$WORKSPACE/archives" - if [[ -d "$archives_dir" ]] && find "$archives_dir" -type f -print -quit | grep -q . ; then - echo "has_archives=true" >> "$GITHUB_OUTPUT" - else - echo "has_archives=false" >> "$GITHUB_OUTPUT" - fi - - - name: AWS S3 Publish Archives - if: ${{ always() && steps.check-archives.outputs.has_archives == 'true' }} - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/aws-s3-publish-logs@1b8eff3ffd9d355b28052be9ad22acc57b3430b5 - with: - aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws_region: ${{ vars.AWS_REGION }} - s3_bucket: ${{ vars.S3_BUCKET }} - s3_path: ${{ env.LOG_NAME }} - archives_path: ${{ env.WORKSPACE }}/archives diff --git a/.github/workflows/vpp-verify-maketest.yml b/.github/workflows/vpp-verify-maketest.yml deleted file mode 100644 index 59f80b772c47..000000000000 --- a/.github/workflows/vpp-verify-maketest.yml +++ /dev/null @@ -1,285 +0,0 @@ -name: vpp-verify-maketest - -on: - workflow_call: - inputs: - FDIO_NAMESPACE: - description: "Executor Namespace (prod|sandbox)" - required: true - type: string - default: prod - GERRIT_BRANCH: - description: "Branch that change is against" - required: true - type: string - GERRIT_CHANGE_NUMBER: - description: "The Gerrit number" - required: true - type: string - GERRIT_PATCHSET_NUMBER: - description: "The patch number for the change" - required: true - type: string - GERRIT_CHANGE_URL: - description: "URL to the change" - required: true - type: string - GERRIT_PROJECT: - description: "Project in Gerrit" - required: true - type: string - GERRIT_REFSPEC: - description: "Gerrit refspec of change" - required: true - type: string - LOG_PREFIX: - description: "Prefix of the log directory" - required: true - type: string - workflow_dispatch: - inputs: - FDIO_NAMESPACE: - description: "Executor Namespace (prod|sandbox)" - required: false - type: choice - options: - - sandbox - - prod - default: prod - GERRIT_BRANCH: - description: "Branch that change is against" - required: true - type: string - GERRIT_CHANGE_NUMBER: - description: "The Gerrit number" - required: true - type: string - GERRIT_PATCHSET_NUMBER: - description: "The patch number for the change" - required: true - type: string - GERRIT_CHANGE_ID: - description: "The ID for the change" - required: true - type: string - GERRIT_CHANGE_URL: - description: "URL to the change" - required: true - type: string - GERRIT_EVENT_TYPE: - description: "Type of Gerrit event" - required: true - type: string - GERRIT_PATCHSET_REVISION: - description: "The revision sha" - required: true - type: string - GERRIT_PROJECT: - description: "Project in Gerrit" - required: true - type: string - GERRIT_REFSPEC: - description: "Gerrit refspec of change" - required: true - type: string - LOG_PREFIX: - description: "Prefix of the log directory" - required: true - type: string - -defaults: - run: - shell: bash - -permissions: - contents: read - actions: read - -jobs: - verify-maketest: - name: ${{ matrix.build_type }}_${{ matrix.os }}_${{ matrix.executor_arch }} - runs-on: - - self-hosted - - nomad - - fdio:arch=${{ matrix.executor_arch }} - - fdio:class=${{ matrix.class }} - - fdio:namespace=${{ inputs.FDIO_NAMESPACE || 'prod' }} - - fdio:os=${{ matrix.os }} - - fdio:workflow=${{ github.run_id }} - - strategy: - fail-fast: false - matrix: - os: ['ubuntu2404', 'ubuntu2204', 'debian12'] - executor_arch: ['x86_64', 'aarch64'] - class: ['builder'] - build_type: ['debug', 'release'] - exclude: - # Exclude debian12 on aarch64 for all builds - - os: 'debian12' - executor_arch: 'aarch64' - # Exclude non-ubuntu2204 OS for debug builds - - build_type: 'debug' - os: 'ubuntu2404' - - build_type: 'debug' - os: 'debian12' - # Exclude aarch64 architecture for debug builds - - build_type: 'debug' - executor_arch: 'aarch64' - - env: - FDIO_NAMESPACE: ${{ inputs.FDIO_NAMESPACE }} - GERRIT_BRANCH: ${{ inputs.GERRIT_BRANCH }} - GERRIT_CHANGE_URL: ${{ inputs.GERRIT_CHANGE_URL }} - GERRIT_CHANGE_NUMBER: ${{ inputs.GERRIT_CHANGE_NUMBER }} - GERRIT_PATCHSET_NUMBER: ${{ inputs.GERRIT_PATCHSET_NUMBER }} - GERRIT_PROJECT: ${{ inputs.GERRIT_PROJECT }} - GERRIT_REFSPEC: ${{ inputs.GERRIT_REFSPEC }} - LOG_PREFIX: ${{ inputs.LOG_PREFIX }} - LOG_NAME: "${{ inputs.LOG_PREFIX }}/verify-maketest-${{ matrix.build_type }}-${{ matrix.class }}-${{ matrix.os }}-${{ inputs.FDIO_NAMESPACE }}-${{ matrix.executor_arch }}" - LOG_DIR: "" - WORKSPACE: /scratch/docker-build/vpp - CCACHE_DIR: /scratch/ccache/${{ matrix.os }}-${{ matrix.executor_arch }} - MAKE_PARALLEL_JOBS: 16 - TEST_JOBS: 4 - TEST_RETRIES: 3 - MAKE_TEST_OS: ${{ matrix.os == 'ubuntu2204' && 'ubuntu-22.04' || 'ubuntu-24.04' }} - MAKE_TEST_MULTIWORKER_OS: 'debian-12' - VPPAPIGEN_TEST_OS: ${{ matrix.os == 'ubuntu2204' && 'ubuntu-22.04' || 'ubuntu-24.04' }} - SHM_SIZE: ${{ matrix.executor_arch == 'aarch64' && '4096M' || '2048M' }} - TUI_LINE: "*******************************************************************" - VPP_TEARDOWN_TIMEOUT: 30 - - steps: - - name: Actions Checkout - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v6.0.2 - - - name: Setup Environment - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/setup-executor-env@b728d1589228e2891a4241a1bba362d76895c225 - - - name: Prepare Log Directory - run: | - set -euxo pipefail - echo "LOG_NAME=$LOG_NAME" >> "$GITHUB_ENV" - LOG_DIR="$WORKSPACE/logs/$LOG_NAME" - echo "LOG_DIR=$LOG_DIR" >> "$GITHUB_ENV" - mkdir -p "$LOG_DIR" - - - name: Show Environment - run: | - set -euo pipefail - env | sort | tee "$LOG_DIR/environment.log" - - - name: Show Nomad Dockerfile - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/show-nomad-dockerfile@b728d1589228e2891a4241a1bba362d76895c225 - - - name: Checkout VPP Gerrit Change - # yamllint disable-line rule:line-length - uses: fdio/vpp/.github/actions/vpp-checkout-gerrit-change@27fc2aa27f57c8f7055e8f1b6e5a9f41b0315873 - with: - WORKSPACE: ${{ env.WORKSPACE }} - GERRIT_BRANCH: ${{ env.GERRIT_BRANCH }} - GERRIT_REFSPEC: ${{ env.GERRIT_REFSPEC }} - TUI_LINE: ${{ env.TUI_LINE }} - - - name: Install VPP dependencies - # yamllint disable-line rule:line-length - uses: fdio/vpp/.github/actions/vpp-install-deps@777792ecef2d5c93ce15fa5b7fdca999f5412c9d - with: - LOG_DIR: ${{ env.LOG_DIR }} - WORKSPACE: ${{ env.WORKSPACE }} - TUI_LINE: ${{ env.TUI_LINE }} - - - name: Install VPP external dependencies - # yamllint disable-line rule:line-length - uses: fdio/vpp/.github/actions/vpp-install-ext-deps@d0577011fb512d79e6914b6f1f11e2be4c09bc1e - with: - LOG_DIR: ${{ env.LOG_DIR }} - WORKSPACE: ${{ env.WORKSPACE }} - TUI_LINE: ${{ env.TUI_LINE }} - - - name: Build VPP - # yamllint disable-line rule:line-length - uses: fdio/vpp/.github/actions/vpp-build@3c0b690a135be65922fec705983478351a1b80b6 - with: - LOG_DIR: ${{ env.LOG_DIR }} - WORKSPACE: ${{ env.WORKSPACE }} - MAKE_PARALLEL_JOBS: ${{ env.MAKE_PARALLEL_JOBS }} - BUILD_TYPE: ${{ matrix['build_type'] }} - TUI_LINE: ${{ env.TUI_LINE }} - - - name: Setup Docker VPP Runtime Environment for 'make test' - # yamllint disable-line rule:line-length - uses: fdio/vpp/.github/actions/vpp-docker-runtime-setup@777792ecef2d5c93ce15fa5b7fdca999f5412c9d - with: - LOG_DIR: ${{ env.LOG_DIR }} - WORKSPACE: ${{ env.WORKSPACE }} - SHM_SIZE: ${{ env.SHM_SIZE }} - TUI_LINE: ${{ env.TUI_LINE }} - - - name: VPP Make Test - # yamllint disable-line rule:line-length - uses: fdio/vpp/.github/actions/vpp-make-test@03f35503fee57df2348617498db28fde557a3880 - timeout-minutes: 120 - with: - LOG_DIR: ${{ env.LOG_DIR }} - WORKSPACE: ${{ env.WORKSPACE }} - BUILD_TYPE: ${{ matrix.build_type }} - TEST_JOBS: ${{ env.TEST_JOBS }} - TEST_RETRIES: ${{ env.TEST_RETRIES }} - MAKE_TEST_OS: ${{ env.MAKE_TEST_OS }} - MAKE_TEST_MULTIWORKER_OS: ${{ env.MAKE_TEST_MULTIWORKER_OS }} - VPPAPIGEN_TEST_OS: ${{ env.VPPAPIGEN_TEST_OS }} - TUI_LINE: ${{ env.TUI_LINE }} - - - name: AWS S3 Publish Logs - if: always() - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/aws-s3-publish-logs@1b8eff3ffd9d355b28052be9ad22acc57b3430b5 - with: - aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws_region: ${{ vars.AWS_REGION }} - s3_bucket: ${{ vars.S3_BUCKET }} - s3_path: ${{ env.LOG_NAME }} - archives_path: ${{ env.LOG_DIR }} - - - name: Check Archives Directory - id: check-archives - if: always() - run: | - set -euxo pipefail - archives_dir="$WORKSPACE/archives" - unittest_failed_dir="/tmp/vpp-failed-unittests" - for failed_test in $(ls "$unittest_failed_dir") - do - unittest_failed_test="$unittest_failed_dir"/"$failed_test" - if [ -f "$unittest_failed_test" ] ; then - cp -a "$unittest_failed_test" "$archives_dir" - else - failed_test_dir="$archives_dir"/"$failed_test" - if [ ! -e "$failed_test_dir" ] ; then - mkdir -p "$failed_test_dir" - cp -a "$unittest_failed_test"/* "$failed_test_dir" - fi - fi - done - if [[ -d "$archives_dir" ]] && find "$archives_dir" -type f -print -quit | grep -q . ; then - echo "has_archives=true" >> "$GITHUB_OUTPUT" - else - echo "has_archives=false" >> "$GITHUB_OUTPUT" - fi - - - name: AWS S3 Publish Archives - if: ${{ always() && steps.check-archives.outputs.has_archives == 'true' }} - # yamllint disable-line rule:line-length - uses: fdio/.github/.github/actions/aws-s3-publish-logs@1b8eff3ffd9d355b28052be9ad22acc57b3430b5 - with: - aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws_region: ${{ vars.AWS_REGION }} - s3_bucket: ${{ vars.S3_BUCKET }} - s3_path: ${{ env.LOG_NAME }} - archives_path: ${{ env.WORKSPACE }}/archives From 0c1ba57a3c8e20b3a7499f6a99e6c1894a974c93 Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 22 Sep 2026 19:28:29 +0800 Subject: [PATCH 72/85] svm: check heap creation in the ssvm init error paths (#32) * svm: unmap the private segment when the heap cannot be created ssvm_server_init_private() maps the private segment with clib_mem_vm_map() and then creates the heap over it. When clib_mem_create_heap() fails the function returned -1 without unmapping, leaking rnd_size + page_size of address space, and -1 is not an SSVM_API_ERROR_* code while the map failure just above returns SSVM_API_ERROR_CREATE_FAILURE. Unmap the mapping before returning and use SSVM_API_ERROR_CREATE_FAILURE, matching the sibling failure path. Type: fix * svm: fail memfd init when the heap cannot be created ssvm_server_init_memfd() stored clib_mem_create_heap() into sh->heap with no check. If it returned 0 the function reported success with sh->heap == 0, and the next ssvm_push_heap() or allocation through the heap dereferenced NULL. Check the result and return SSVM_API_ERROR_CREATE_FAILURE. The mapping and the fd are unmapped and closed on that path because neither caller cleans up after a failure: socket_api.c returns on rv != 0 without ssvm_delete_memfd(), and selog.c returns the error without it. The other failure paths in this function close memfd->fd for the same reason. Type: fix --- src/svm/ssvm.c | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/svm/ssvm.c b/src/svm/ssvm.c index 829cec54d1e0..7c00cacd2f3c 100644 --- a/src/svm/ssvm.c +++ b/src/svm/ssvm.c @@ -274,6 +274,14 @@ ssvm_server_init_memfd (ssvm_private_t * memfd) sh->heap = clib_mem_create_heap (((u8 *) sh) + page_size, memfd->ssvm_size - page_size, 1 /* locked */ , "ssvm server memfd"); + if (sh->heap == 0) + { + clib_unix_warning ("heap alloc"); + clib_mem_vm_unmap (memfd->sh); + close (memfd->fd); + memfd->fd = -1; + return SSVM_API_ERROR_CREATE_FAILURE; + } oldheap = ssvm_push_heap (sh); sh->name = format (0, "%s", memfd->name, 0); ssvm_pop_heap (oldheap); @@ -389,7 +397,8 @@ ssvm_server_init_private (ssvm_private_t * ssvm) if (heap == 0) { clib_unix_warning ("heap alloc"); - return -1; + clib_mem_vm_unmap (sh); + return SSVM_API_ERROR_CREATE_FAILURE; } rnd_size = clib_mem_get_heap_free_space (heap); From 18147b33f19300bab07c9e029304f749e826f287 Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 22 Sep 2026 19:32:57 +0800 Subject: [PATCH 73/85] vcl: honour optlen in getsockopt writes (#31) getsockopt() writes into the caller's optval buffer without honouring the caller's *optlen, which is the size of that buffer. TCP_CONGESTION overwrote *optlen with strlen("cubic") before copying, so strncpy() wrote 7 bytes ("cubic" and its terminator) into a buffer that may be only 4 bytes. Check *optlen against sizeof("cubic") before the copy, use clib_memcpy_fast() as the rest of the file does, and keep the returned *optlen at strlen("cubic") = 5. The neighbouring NULL-argument case returns -EFAULT; a too-small buffer is EINVAL, so this path returns -EINVAL. SO_PROTOCOL called vls_attr(VPPCOM_ATTR_GET_PROTOCOL) and then stored an int unconditionally, including when vls_attr had returned VPPCOM_EINVAL for a short buffer. vls_attr guarantees *optlen >= sizeof(int) only on its success path (vppcom.c:4135), so guard the store on rv == VPPCOM_OK and the bound. The remaining option cases delegate to vls_attr with the caller's optlen, and TCP_INFO memsets *optlen bytes, so no other fixed-size write ignores the caller's buffer size. Type: fix --- src/vcl/ldp.c | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/vcl/ldp.c b/src/vcl/ldp.c index 210c6a2d5871..dc258c28f2df 100644 --- a/src/vcl/ldp.c +++ b/src/vcl/ldp.c @@ -2038,8 +2038,13 @@ getsockopt (int fd, int level, int optname, rv = -EFAULT; break; case TCP_CONGESTION: + if (*optlen < (socklen_t) sizeof ("cubic")) + { + rv = -EINVAL; + break; + } + clib_memcpy_fast (optval, "cubic", sizeof ("cubic")); *optlen = strlen ("cubic"); - strncpy (optval, "cubic", *optlen + 1); rv = 0; break; default: @@ -2086,7 +2091,8 @@ getsockopt (int fd, int level, int optname, break; case SO_PROTOCOL: rv = vls_attr (vlsh, VPPCOM_ATTR_GET_PROTOCOL, optval, optlen); - *(int *) optval = *(int *) optval ? SOCK_DGRAM : SOCK_STREAM; + if (rv == VPPCOM_OK && *optlen >= sizeof (int)) + *(int *) optval = *(int *) optval ? SOCK_DGRAM : SOCK_STREAM; break; case SO_SNDBUF: rv = vls_attr (vlsh, VPPCOM_ATTR_GET_TX_FIFO_LEN, From 299cba9653614d385fb3a15f037272ac45a3b1ba Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 22 Sep 2026 19:33:13 +0800 Subject: [PATCH 74/85] vcl: do not unlock workers_lock before locking it (#33) vcl_send_worker_rpc() jumps to done: when data_len exceeds sizeof(mp->data), but done: calls clib_spinlock_unlock(&vcm->workers_lock) unconditionally. On that path the lock was never taken, so clib_spinlock_unlock stores 0 into a lock this call does not hold: if another worker holds workers_lock, its lock is silently released and mutual exclusion for every other user of the lock is broken for the duration. Return -1 directly from the length check instead, before the lock is taken. This is currently unreachable because every caller passes far less than sizeof(mp->data), but the invariant to unlock only what was locked must hold regardless. The other goto done in this function follows the lock and is unchanged. Type: fix --- src/vcl/vppcom.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vcl/vppcom.c b/src/vcl/vppcom.c index f834910c7f60..00d5c4728128 100644 --- a/src/vcl/vppcom.c +++ b/src/vcl/vppcom.c @@ -295,7 +295,7 @@ vcl_send_worker_rpc (u32 dst_wrk_index, void *data, u32 data_len) int ret = -1; if (data_len > sizeof (mp->data)) - goto done; + return -1; clib_spinlock_lock (&vcm->workers_lock); From 877fad4d2654b42827e3e9f3c28e9bbfab1b2272 Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 22 Sep 2026 19:43:33 +0800 Subject: [PATCH 75/85] vapi: use the payload-only buffer in the socket disconnect drain (#35) vapi_sock_recv_internal() returns the message payload only: it does vec_add (*vec_msg, mbp->data, data_len) at vapi.c:554 and drops the msgbuf_t header, and the connect path reads it that way (vapi.c:800, rp = (void *) msg). vapi_sock_client_disconnect() instead treated that payload vector as header-bearing: rp = (void *) msgbuf->data put rp 16 bytes into the payload, the message-id test at vapi.c:935 read the wrong bytes so it never matched VL_API_SOCKCLNT_DELETE_REPLY, and ntohl (msgbuf->data_len) took the length from payload bytes and passed it to vl_msg_api_handler(). Use rp = (void *) msg, matching the connect path, and do not route the drained messages through vl_msg_api_handler(). That handler passes free_it=1, so its free path runs vl_msg_api_free, which computes rv = a - offsetof (msgbuf_t, data), reads rv->q and then clib_mem_free (rv) (src/vlibapi/memory_shared.c:314, 320, 328). A vector produced by vec_add has no msgbuf_t in front of it, so that would read and free memory the client does not own. Free the vector with vec_free() instead and skip dispatching; the drain only needs the reply id to detect the delete reply. Type: fix --- src/vpp-api/vapi/vapi.c | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/vpp-api/vapi/vapi.c b/src/vpp-api/vapi/vapi.c index 1ce722ee915a..bc6e4fa8157b 100644 --- a/src/vpp-api/vapi/vapi.c +++ b/src/vpp-api/vapi/vapi.c @@ -905,7 +905,6 @@ vapi_sock_client_disconnect (vapi_ctx_t ctx) { vl_api_sockclnt_delete_reply_t *rp; u8 *msg = 0; - msgbuf_t *msgbuf; int rv; f64 deadline; @@ -929,18 +928,15 @@ vapi_sock_client_disconnect (vapi_ctx_t ctx) if (vapi_sock_recv_internal (ctx, &msg, 0) != VAPI_OK) continue; - msgbuf = (void *) msg; - rp = (void *) msgbuf->data; + rp = (void *) msg; /* drain the queue */ if (ntohs (rp->_vl_msg_id) != VL_API_SOCKCLNT_DELETE_REPLY) { clib_warning ("queue drain: %d", ntohs (rp->_vl_msg_id)); - msgbuf = (msgbuf_t *) ((u8 *) rp - offsetof (msgbuf_t, data)); - vl_msg_api_handler ((void *) rp, ntohl (msgbuf->data_len)); + vec_free (msg); continue; } - msgbuf = (msgbuf_t *) ((u8 *) rp - offsetof (msgbuf_t, data)); - vl_msg_api_handler ((void *) rp, ntohl (msgbuf->data_len)); + vec_free (msg); break; } From 6cf3d9ba88b2a439887c3d1217ada6bb38bbacf5 Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 22 Sep 2026 19:44:43 +0800 Subject: [PATCH 76/85] linux-cp: use -1 as the default namespace fd sentinel (#34) lcp_set_default_ns() stored a raw open() result, but a successful open() returns 0 whenever descriptor 0 is free, which happens when VPP is started detached or with stdin closed. 0 was also the "no default namespace" sentinel: the teardown close was guarded by > 0, so that namespace read as unset, the fd was leaked, and the consumers at lcp_nl.c:942 and :1063 skipped the setns switch and programmed the default namespace instead. Use -1 as the unset sentinel: initialise lcp_main.default_ns_fd to -1 (it is the only initialiser; there is no init function for this field), keep the open() result, test >= 0 at the teardown close and at both consumers, and reset to -1. A failure open() still stores -1, which >= 0 correctly rejects, so 9585f21cd's caller-side comparison stays correct. Type: fix --- src/plugins/linux-cp/lcp.c | 8 +++++--- src/plugins/linux-cp/lcp_nl.c | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/plugins/linux-cp/lcp.c b/src/plugins/linux-cp/lcp.c index 712969cba8db..016dadeb32d3 100644 --- a/src/plugins/linux-cp/lcp.c +++ b/src/plugins/linux-cp/lcp.c @@ -16,7 +16,9 @@ #include #include -lcp_main_t lcp_main; +lcp_main_t lcp_main = { + .default_ns_fd = -1, +}; u8 * lcp_get_default_ns (void) @@ -56,9 +58,9 @@ lcp_set_default_ns (u8 *ns) if (!p || *p == 0) { lcpm->default_namespace = NULL; - if (lcpm->default_ns_fd > 0) + if (lcpm->default_ns_fd >= 0) close (lcpm->default_ns_fd); - lcpm->default_ns_fd = 0; + lcpm->default_ns_fd = -1; return 0; } diff --git a/src/plugins/linux-cp/lcp_nl.c b/src/plugins/linux-cp/lcp_nl.c index 50a7e5e7ba45..9da9f2376bb7 100644 --- a/src/plugins/linux-cp/lcp_nl.c +++ b/src/plugins/linux-cp/lcp_nl.c @@ -939,7 +939,7 @@ lcp_nl_open_socket (void) nl_socket_disable_seq_check (nm->sk_route); dest_ns_fd = lcp_get_default_ns_fd (); - if (dest_ns_fd > 0) + if (dest_ns_fd >= 0) { curr_ns_fd = clib_netns_open (NULL /* self */); if (curr_ns_fd < 0) @@ -1060,7 +1060,7 @@ lcp_nl_open_sync_socket (nl_sock_type_t sock_type) return -NLE_NOMEM; dest_ns_fd = lcp_get_default_ns_fd (); - if (dest_ns_fd > 0) + if (dest_ns_fd >= 0) { curr_ns_fd = clib_netns_open (NULL /* self */); if (curr_ns_fd < 0) From b72d1f6c29f8cc2b9e14fb6d1faca83a579ae517 Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 22 Sep 2026 20:08:08 +0800 Subject: [PATCH 77/85] gha: run pr-verify steps under bash, not dash (#36) * gha: drop workflows that cannot run on this fork Sixteen of the seventeen workflows come from upstream FD.io and target its self-hosted Nomad runners (runs-on: [self-hosted, nomad, fdio:...]). They also consume the fdio/* remote composite actions and the AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, PACKAGECLOUD_TOKEN and FDIO_VPP_COVERITY_TOKEN secrets. This fork has no runners with those labels and no Actions secrets, so every one of those jobs sits queued forever: the periodic coverity and asan jobs were observed queued for over an hour with no runner ever picking them up. Only the Debian 13 DEB workflow runs on GitHub-hosted runners and completes. Remove the sixteen that cannot execute here so the workflow list shows only what this fork can actually run. Type: ci * gha: drop the unused Gerrit checkout action The vpp-checkout-gerrit-change composite action reuses the Gerrit-only refspec, branch and event inputs that the workflows removed in the previous commit supplied. Nothing references it any more, and this fork verifies GitHub pull requests, not Gerrit changes. Type: ci * gha: add a pull request verification workflow This fork has no pull request verification. The sixteen workflows removed earlier could not run here, and only the Debian 13 DEB build workflow executed on a GitHub-hosted runner. Add pr-verify.yml, which runs on GitHub-hosted runners and has a fast style job and a combined build-and-test job. The style job runs the repo's own gates (checkstyle, checkstyle-commit, checkfeaturelist, checkstyle-api and checkstyle-python) against the pull request's commit range. The build-and-test job installs the Debian 13 dependencies, builds VPP in debug mode with ccache keyed by commit the way the DEB workflow keys it, and runs the Python functional tests with a focused TEST filter derived from the changed paths, falling back to an ip4 smoke suite. A per-ref concurrency group cancels a superseded run. The temporary push trigger on ci/** exists only while the workflow is brought up and is removed before the pull request is opened. Type: ci * gha: fix bring-up failures in the PR workflow make checkstyle-api runs crcchecker.py, which refuses to run when a tracked file is modified. An earlier step edited the Makefile, so the working tree was dirty and the check failed with "Please stash or commit changes in workspace". The edit is not needed in this job, so drop it and leave the tree clean. The build failed at CMake configure with "VPP version : none": src/scripts/version calls git describe without --tags, and the origin of this fork has no v* tags. The DEB workflow already handles the same clone, so write src/scripts/.version in its format when no tag is reachable. Type: fix * gha: finalise the pull request verification workflow Drop the temporary push trigger that let a plain push to a ci/* branch run the workflow while it was brought up. The workflow now triggers on pull_request and workflow_dispatch only. Skip a cache save when its exact key was restored. Asking to save a key that already exists fails the step with "Unable to reserve cache", which happened on the second run against an unchanged Makefile. The keys themselves are unchanged. Type: ci * gha: run the workflow steps under bash, not dash A step in this workflow failed with a bare "Bad substitution" and exit code 2, unrelated to the pull request it was checking. Both jobs declare a debian:trixie container, and a container job gets sh for its run: steps unless it says otherwise. debian:trixie's /bin/sh is dash, which does not implement the parameter expansions this workflow and the repository's own scripts use, so the step aborted at the first one. Set defaults.run.shell to bash for both jobs so no step can fall back to sh, and install bash explicitly in the base-tools step rather than depending on what the image happens to carry. Do not rewrite the expansions into POSIX sh: the workflow is written for bash and the fix is to run it under the shell it is written for. This also removes the workflow_dispatch trigger. GitHub resolves the workflow file at the requested ref, so dispatching a branch created before this file existed fails with HTTP 422, and when it does run, checkstyle diffs only HEAD^, which silently skips the earlier commits of a multi-commit branch. Closing and reopening a pull request fires the reopened type and runs the workflow from the merge commit, which checks the whole pull request, so one correct entry point is left instead of a second subtly-wrong one. The commit-message check now walks git rev-list --reverse --no-merges. A merge commit's subject has no feature id, so check_commit_msg.sh rejected it, and a contributor who merges the base branch into their pull request got a red check for a reason unrelated to their change. Type: fix --- .github/workflows/pr-verify.yml | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pr-verify.yml b/.github/workflows/pr-verify.yml index 9d759a509a1f..107765eea832 100644 --- a/.github/workflows/pr-verify.yml +++ b/.github/workflows/pr-verify.yml @@ -5,7 +5,6 @@ on: pull_request: branches: [master] types: [opened, synchronize, reopened] - workflow_dispatch: # One run per ref: a new push to the same branch cancels the previous run. concurrency: @@ -21,6 +20,12 @@ jobs: runs-on: ubuntu-24.04 container: image: debian:trixie + # A job that runs in a container gets `sh` for `run:` unless it says + # otherwise, and debian:trixie's /bin/sh is dash, which rejects the + # parameter expansions this repository's scripts use. + defaults: + run: + shell: bash env: # extras/scripts/checkstyle.sh picks clang-format-${CLANG_FORMAT_VER}; # the Makefile sets 19 for Debian 13, and the same value has to reach the @@ -33,7 +38,7 @@ jobs: - name: Install base tools run: | apt-get update -qq - apt-get install -y -qq sudo make git + apt-get install -y -qq sudo make git bash - name: Check out the pull request uses: actions/checkout@v4 @@ -79,7 +84,10 @@ jobs: fi checked_out=$(git rev-parse HEAD) trap 'git checkout --quiet "${checked_out}"' EXIT - for commit in $(git rev-list --reverse "${base}..${head}"); do + # A merge commit has no feature id in its subject, and + # check_commit_msg.sh rejects a subject without one, so only the + # commits the pull request author wrote are checked. + for commit in $(git rev-list --reverse --no-merges "${base}..${head}"); do echo "Checking commit message of ${commit}" git checkout --quiet "${commit}" extras/scripts/check_commit_msg.sh @@ -93,6 +101,9 @@ jobs: # The test framework reserves SHM_PER_PROCESS per worker on top of a # 1 GiB minimum; the 64 MiB container default is far too small. options: --shm-size=2g + defaults: + run: + shell: bash env: CCACHE_COMPRESS: "true" CCACHE_MAXSIZE: 2G @@ -103,7 +114,7 @@ jobs: - name: Install base tools run: | apt-get update -qq - apt-get install -y -qq sudo make git + apt-get install -y -qq sudo make git bash - name: Check out the pull request uses: actions/checkout@v4 From ffabf0ef23f6d9500f7375f5f1348e6b7fab919e Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 22 Sep 2026 20:17:07 +0800 Subject: [PATCH 78/85] fib: validate the fib table index at interface-derived lookups (#26) * fib: reject a bad index in the table-id accessors fib_table_get() indexes the per-protocol FIB pool without validating the index, so an index of ~0 computes a wild pointer and every caller dereferences it. ~0 is reachable: fib_table_get_index_for_sw_if_index() returns it for an interface that is not bound to a table or that has been deleted. Promote fib_table_get_or_null(), which already rejected ~0, an out-of-range index and a free pool slot, and route fib_table_get_table_id() and fib_table_get_table_id_for_sw_if_index() through it. The NULL check both already had is then live instead of dead. Type: fix * fib: check the index at interface-derived callers Adopt fib_table_get_or_null() where the FIB index comes from an interface binding: the sw_interface_get_table API handler, the ip6 link-local show path, and the show ip table CLI. Each now tests the returned pointer and skips the entry or returns an error instead of dereferencing a table that may no longer exist. Type: fix * fib: satisfy clang-format in the lookup error path clang-format 19.1.0 keeps the clib_error_return call on one line; it fits in 100 columns. This is the only formatting change the #26 review asked for. Type: style --- src/vnet/fib/fib_table.c | 6 +++--- src/vnet/fib/fib_table.h | 12 ++++++++++++ src/vnet/interface_api.c | 5 +++-- src/vnet/ip/ip6_ll_table.c | 4 ++-- src/vnet/ip/lookup.c | 8 +++++++- 5 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/vnet/fib/fib_table.c b/src/vnet/fib/fib_table.c index 44f4e4485fd2..939448ee0870 100644 --- a/src/vnet/fib/fib_table.c +++ b/src/vnet/fib/fib_table.c @@ -68,7 +68,7 @@ fib_table_lookup_i (fib_table_t *fib_table, * its binding is torn down. Validate here so that a bad index produces an * empty lookup rather than a read through a wild pointer. */ -static fib_table_t * +fib_table_t * fib_table_get_or_null (u32 fib_index, fib_protocol_t proto) { if (~0 == fib_index) @@ -1137,7 +1137,7 @@ fib_table_get_table_id_for_sw_if_index (fib_protocol_t proto, { fib_table_t *fib_table; - fib_table = fib_table_get(fib_table_get_index_for_sw_if_index( + fib_table = fib_table_get_or_null(fib_table_get_index_for_sw_if_index( proto, sw_if_index), proto); @@ -1150,7 +1150,7 @@ fib_table_get_table_id (u32 fib_index, { fib_table_t *fib_table; - fib_table = fib_table_get(fib_index, proto); + fib_table = fib_table_get_or_null(fib_index, proto); return ((NULL != fib_table ? fib_table->ft_table_id : ~0)); } diff --git a/src/vnet/fib/fib_table.h b/src/vnet/fib/fib_table.h index b22dd7f71d5c..24510dba10b4 100644 --- a/src/vnet/fib/fib_table.h +++ b/src/vnet/fib/fib_table.h @@ -904,6 +904,18 @@ extern u32 fib_table_get_num_entries(u32 fib_index, extern fib_table_t *fib_table_get(fib_node_index_t index, fib_protocol_t proto); +/** + * @brief + * Get a pointer to a FIB table, validating the index + * + * Unlike fib_table_get(), this rejects ~0, an out-of-range index and a + * free pool slot, and returns NULL instead. Callers that derive the index + * from an interface binding or from another untrusted source must use this + * accessor. + */ +extern fib_table_t *fib_table_get_or_null(u32 fib_index, + fib_protocol_t proto); + /** * @brief return code controlling how a table walk proceeds */ diff --git a/src/vnet/interface_api.c b/src/vnet/interface_api.c index d7f6234f37ba..f17855867d7a 100644 --- a/src/vnet/interface_api.c +++ b/src/vnet/interface_api.c @@ -664,8 +664,9 @@ vl_api_sw_interface_get_table_t_handler (vl_api_sw_interface_get_table_t * mp) fib_index = fib_table_get_index_for_sw_if_index (fib_proto, sw_if_index); if (fib_index != ~0) { - fib_table = fib_table_get (fib_index, fib_proto); - table_id = fib_table->ft_table_id; + fib_table = fib_table_get_or_null (fib_index, fib_proto); + if (fib_table) + table_id = fib_table->ft_table_id; } BAD_SW_IF_INDEX_LABEL; diff --git a/src/vnet/ip/ip6_ll_table.c b/src/vnet/ip/ip6_ll_table.c index b7ecd8595c61..efbdb88c02bf 100644 --- a/src/vnet/ip/ip6_ll_table.c +++ b/src/vnet/ip/ip6_ll_table.c @@ -246,9 +246,9 @@ ip6_ll_show_fib (vlib_main_t * vm, if (~0 == fib_index) continue; - fib_table = fib_table_get (fib_index, FIB_PROTOCOL_IP6); + fib_table = fib_table_get_or_null (fib_index, FIB_PROTOCOL_IP6); - if (!(fib_table->ft_flags & FIB_TABLE_FLAG_IP6_LL)) + if (NULL == fib_table || !(fib_table->ft_flags & FIB_TABLE_FLAG_IP6_LL)) continue; ip6_fib_table_show (vm, fib_table, !verbose); diff --git a/src/vnet/ip/lookup.c b/src/vnet/ip/lookup.c index 57db3deed401..70817dbe6e05 100644 --- a/src/vnet/ip/lookup.c +++ b/src/vnet/ip/lookup.c @@ -500,7 +500,13 @@ vnet_show_ip_table_cmd (vlib_main_t *vm, unformat_input_t *main_input, goto done; } - fib = fib_table_get (fib_index, fproto); + fib = fib_table_get_or_null (fib_index, fproto); + if (NULL == fib) + { + error = clib_error_return (0, "table with table_id %u no longer exists", table_id); + goto done; + } + vlib_cli_output (vm, "[%u] table_id:%u %v", fib->ft_index, fib->ft_table_id, fib->ft_desc); } From 937348fa95c4cfce42ef829a94f4726596beda15 Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 22 Sep 2026 20:17:26 +0800 Subject: [PATCH 79/85] span: validate sw_if_index in the enable/disable API (#30) vl_api_sw_interface_span_enable_disable_t_handler passed the client-supplied sw_if_index_from straight to span_add_delete_entry, which sizes sm->interfaces with vec_validate_aligned() on that index (span.c:55). Any large index other than the ~0 sentinel made VPP grow the vector to that size; the allocation failed and VPP aborted. sw_if_index_to reaches clib_bitmap_set() in span_dst_set (span.c:33), which grows the mirror_ports bitmap the same way, so it needs the same bound when it is not the ~0 sentinel. Validate both with vnet_sw_if_index_is_api_valid() at the API boundary and return VNET_API_ERROR_INVALID_SW_IF_INDEX, mirroring VALIDATE_SW_IF_INDEX. The ~0 sentinel is still accepted for sw_if_index_to, because it means clear-all when state is 0. span_add_delete_entry is unchanged; the CLI path already unformats valid interface indices. span.api is not touched, so the message CRC is unchanged. Type: fix --- src/vnet/span/span_api.c | 11 ++++++++++- test/test_span.py | 19 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/vnet/span/span_api.c b/src/vnet/span/span_api.c index 7bfb6f313902..e783a9c1f0aa 100644 --- a/src/vnet/span/span_api.c +++ b/src/vnet/span/span_api.c @@ -25,14 +25,23 @@ static void (vl_api_sw_interface_span_enable_disable_t * mp) { vl_api_sw_interface_span_enable_disable_reply_t *rmp; - int rv; + int rv = 0; vlib_main_t *vm = vlib_get_main (); + if (!vnet_sw_if_index_is_api_valid (ntohl (mp->sw_if_index_from)) || + (ntohl (mp->sw_if_index_to) != ~0 && + !vnet_sw_if_index_is_api_valid (ntohl (mp->sw_if_index_to)))) + { + rv = VNET_API_ERROR_INVALID_SW_IF_INDEX; + goto bad_sw_if_index; + } + rv = span_add_delete_entry (vm, ntohl (mp->sw_if_index_from), ntohl (mp->sw_if_index_to), ntohl (mp->state), mp->is_l2 ? SPAN_FEAT_L2 : SPAN_FEAT_DEVICE); + BAD_SW_IF_INDEX_LABEL; REPLY_MACRO (VL_API_SW_INTERFACE_SPAN_ENABLE_DISABLE_REPLY); } diff --git a/test/test_span.py b/test/test_span.py index c657b3495f04..f25f1f611911 100644 --- a/test/test_span.py +++ b/test/test_span.py @@ -15,6 +15,7 @@ from vpp_vxlan_tunnel import VppVxlanTunnel from collections import namedtuple from vpp_papi import VppEnum +from vpp_papi_exceptions import UnexpectedApiReturnValueError from config import config Tag = namedtuple("Tag", ["dot1", "vlan"]) @@ -208,6 +209,24 @@ def test_device_span(self): self.verify_capture(pg1_pkts, pg2_pkts) + def test_span_api_invalid_index(self): + """SPAN API rejects an invalid sw_if_index from/to""" + + # larger than any interface, and deliberately not the ~0 sentinel + invalid_sw_if_index = 0xFFFFFFFE + + with self.assertRaises(UnexpectedApiReturnValueError) as e: + self.vapi.sw_interface_span_enable_disable( + invalid_sw_if_index, self.pg2.sw_if_index + ) + self.assertEqual(e.exception.retval, -2) # VNET_API_ERROR_INVALID_SW_IF_INDEX + + with self.assertRaises(UnexpectedApiReturnValueError) as e: + self.vapi.sw_interface_span_enable_disable( + self.pg0.sw_if_index, invalid_sw_if_index + ) + self.assertEqual(e.exception.retval, -2) + def test_span_l2_rx(self): """SPAN l2 rx mirror""" From 46177b1e6e1b3f9f7a600e8aadf7c3d0c6f90c9e Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 22 Sep 2026 20:17:30 +0800 Subject: [PATCH 80/85] api: bound the API message length before tracing (#28) * api: bound the API message length before tracing The shared-memory API dequeue passed the raw message pointer and the client-written msgbuf_t.data_len to vl_msg_api_trace(), which copies data_len bytes from the message. A client that announces a large data_len therefore makes VPP allocate and read up to 4 GiB from a shorter source, and the unconditional VL_MSG_API_UNPOISON reads sizeof(msgbuf_t) + data_len as well. Only rx tracing triggers the trace copy, but the unpoison runs regardless of tracing. Validate the message pointer and its announced length against the shared region on dequeue, before either use. The bound comes from VPP-owned state, vlib_rp->virtual_base and virtual_size; msgbuf_t.q and data_len are client-writable and are not a usable bound. Thread the validated length into vl_mem_api_handler_with_vm_node and trace with vl_msg_api_trace_with_size() instead of re-deriving it from the header. The RPC path validates its queued messages the same way. A message that fails validation is dropped without being freed: vl_msg_api_free() trusts the client-written msgbuf_t.q. The dequeue returns 0 rather than -1 so the caller keeps draining the queue instead of sleeping on a forged empty read. Type: fix * api: reflow the message-length trace change clang-format 19.1.7, the pinned distro version used by extras/scripts/checkstyle.sh, reflows the declarations and calls this change touches: the api.h declarations, the vl_msg_api_trace_with_size definition, and the vl_mem_api_handler_with_vm_node signature and call sites. Formatting only, no behaviour change. checkstyle.sh reports SUCCESS after the reflow. Type: style --- src/vlibapi/api.h | 4 +-- src/vlibapi/api_shared.c | 5 ++- src/vlibmemory/memory_api.c | 61 ++++++++++++++++++++++++++++++------- 3 files changed, 54 insertions(+), 16 deletions(-) diff --git a/src/vlibapi/api.h b/src/vlibapi/api.h index 44e07531dc9f..4270a44f0467 100644 --- a/src/vlibapi/api.h +++ b/src/vlibapi/api.h @@ -88,8 +88,8 @@ vl_msg_api_get_msg_length_inline (void *msg_arg) int vl_msg_api_rx_trace_enabled (api_main_t * am); int vl_msg_api_tx_trace_enabled (api_main_t * am); void vl_msg_api_trace (api_main_t * am, vl_api_trace_t * tp, void *msg); -int vl_msg_api_trace_onoff (api_main_t * am, vl_api_trace_which_t which, - int onoff); +void vl_msg_api_trace_with_size (api_main_t *am, vl_api_trace_t *tp, void *msg, uword msg_len); +int vl_msg_api_trace_onoff (api_main_t *am, vl_api_trace_which_t which, int onoff); int vl_msg_api_trace_free (api_main_t * am, vl_api_trace_which_t which); int vl_msg_api_trace_configure (api_main_t * am, vl_api_trace_which_t which, u32 nitems); diff --git a/src/vlibapi/api_shared.c b/src/vlibapi/api_shared.c index 9e57374f2ffd..a6e9addf3b4c 100644 --- a/src/vlibapi/api_shared.c +++ b/src/vlibapi/api_shared.c @@ -58,9 +58,8 @@ vl_msg_api_tx_trace_enabled (api_main_t * am) /* * vl_msg_api_trace */ -static void -vl_msg_api_trace_with_size (api_main_t *am, vl_api_trace_t *tp, void *msg, - uword msg_len) +void +vl_msg_api_trace_with_size (api_main_t *am, vl_api_trace_t *tp, void *msg, uword msg_len) { u8 **this_trace; u8 **old_trace; diff --git a/src/vlibmemory/memory_api.c b/src/vlibmemory/memory_api.c index 0ae7fe6f3703..451e38ba814d 100644 --- a/src/vlibmemory/memory_api.c +++ b/src/vlibmemory/memory_api.c @@ -751,9 +751,9 @@ void (*vl_mem_api_fuzz_hook) (u16, void *); /* This is only to be called from a vlib/vnet app */ static void -vl_mem_api_handler_with_vm_node (api_main_t *am, svm_region_t *vlib_rp, - void *the_msg, vlib_main_t *vm, - vlib_node_runtime_t *node, u8 is_private) +vl_mem_api_handler_with_vm_node (api_main_t *am, svm_region_t *vlib_rp, void *the_msg, + vlib_main_t *vm, vlib_node_runtime_t *node, u8 is_private, + uword msg_len) { u16 id = clib_net_to_host_u16 (*((u16 *) the_msg)); vl_api_msg_data_t *m = vl_api_get_msg_data (am, id); @@ -784,7 +784,7 @@ vl_mem_api_handler_with_vm_node (api_main_t *am, svm_region_t *vlib_rp, handler = (void *) m->handler; if (PREDICT_FALSE (am->rx_trace && am->rx_trace->enabled)) - vl_msg_api_trace (am, am->rx_trace, the_msg); + vl_msg_api_trace_with_size (am, am->rx_trace, the_msg, msg_len); if (PREDICT_FALSE (am->msg_print_flag)) { @@ -880,10 +880,35 @@ vl_mem_api_handler_with_vm_node (api_main_t *am, svm_region_t *vlib_rp, } } +/* + * Validate that the API message at mp and the body length it announces lie + * inside the shared region vlib_rp. Both mp and msgbuf_t.data_len are + * written by the API client, so neither is trusted: a bogus pointer, or a + * bogus length, would otherwise make the trace path and VL_MSG_API_UNPOISON + * read outside the region. Returns 0 and stores the length on success. + */ +static int +vl_mem_api_msg_len (svm_region_t *vlib_rp, uword mp, uword *msg_len) +{ + uword region_start = (uword) vlib_rp->virtual_base; + uword region_end = region_start + vlib_rp->virtual_size; + msgbuf_t *hdr; + + if (mp < region_start + sizeof (msgbuf_t) || mp > region_end) + return -1; + + hdr = (msgbuf_t *) (mp - sizeof (msgbuf_t)); + *msg_len = (uword) clib_net_to_host_u32 (hdr->data_len); + + if (*msg_len > region_end - mp) + return -1; + + return 0; +} + static inline int -void_mem_api_handle_msg_i (api_main_t * am, svm_region_t * vlib_rp, - vlib_main_t * vm, vlib_node_runtime_t * node, - u8 is_private) +void_mem_api_handle_msg_i (api_main_t *am, svm_region_t *vlib_rp, vlib_main_t *vm, + vlib_node_runtime_t *node, u8 is_private) { svm_queue_t *q; uword mp; @@ -892,9 +917,16 @@ void_mem_api_handle_msg_i (api_main_t * am, svm_region_t * vlib_rp, if (!svm_queue_sub2 (q, (u8 *) & mp)) { + uword msg_len; + + if (vl_mem_api_msg_len (vlib_rp, mp, &msg_len)) + { + clib_warning ("dropping API message outside the region (0x%lx)", mp); + return 0; + } + VL_MSG_API_UNPOISON ((void *) mp); - vl_mem_api_handler_with_vm_node (am, vlib_rp, (void *) mp, vm, node, - is_private); + vl_mem_api_handler_with_vm_node (am, vlib_rp, (void *) mp, vm, node, is_private, msg_len); return 0; } return -1; @@ -942,9 +974,16 @@ vl_mem_api_handle_rpc (vlib_main_t * vm, vlib_node_runtime_t * node) vl_msg_api_barrier_sync (); for (i = 0; i < vec_len (vm->processing_rpc_requests); i++) { + uword msg_len; + mp = vm->processing_rpc_requests[i]; - vl_mem_api_handler_with_vm_node (am, am->vlib_rp, (void *) mp, vm, - node, 0 /* is_private */); + if (vl_mem_api_msg_len (am->vlib_rp, mp, &msg_len)) + { + clib_warning ("dropping RPC message outside the region (0x%lx)", mp); + continue; + } + vl_mem_api_handler_with_vm_node (am, am->vlib_rp, (void *) mp, vm, node, + 0 /* is_private */, msg_len); } vl_msg_api_barrier_release (); } From ab774c3acab42da74cad45b9c4e5d3498e370355 Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 22 Sep 2026 20:48:15 +0800 Subject: [PATCH 81/85] nat: guard the nat64 BIB/ST logging fib read (#27) * fib: reject a bad index in the table-id accessors fib_table_get() indexes the per-protocol FIB pool without validating the index, so an index of ~0 computes a wild pointer and every caller dereferences it. ~0 is reachable: fib_table_get_index_for_sw_if_index() returns it for an interface that is not bound to a table or that has been deleted. Promote fib_table_get_or_null(), which already rejected ~0, an out-of-range index and a free pool slot, and route fib_table_get_table_id() and fib_table_get_table_id_for_sw_if_index() through it. The NULL check both already had is then live instead of dead. Type: fix * fib: check the index at interface-derived callers Adopt fib_table_get_or_null() where the FIB index comes from an interface binding: the sw_interface_get_table API handler, the ip6 link-local show path, and the show ip table CLI. Each now tests the returned pointer and skips the entry or returns an error instead of dereferencing a table that may no longer exist. Type: fix * nat: guard the nat64 BIB/ST logging fib read nat64_db.c resolves the FIB table of a BIB/ST entry to pass its table id to the IPFIX logging call. The fib index of a BIB entry created from a packet comes from the packet's ingress interface (nat64_in2out.c:178-180), which is ~0 for an interface with no IPv6 table binding. fib_table_get() does not validate the index, so pool_elt_at_index (ip6_main.fibs, ~0) computes a wild pointer and fib->ft_table_id is read through it. The read is an argument to the logging call, so it happens whether or not IPFIX logging is enabled. Use fib_table_get_or_null() at the four sites and skip only the log record when the table cannot be resolved. The database operation is unchanged: no early return and no table-id 0 substitution, because 0 is a valid FIB table id. Type: fix * nat: reflow nat64 logging guards for clang-format clang-format 19.1.7, the pinned distro version, reflows the argument lists of the guarded nat_ipfix_logging_nat64_bib()/session() calls and of the two adjacent nat_syslog_nat64_sadd()/sdel() calls that fall in the same hunks. Formatting only, no behaviour change. extras/scripts/checkstyle.sh reports SUCCESS after the reflow. Type: style --- src/plugins/nat/nat64/nat64_db.c | 49 ++++++++++++++------------------ 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/src/plugins/nat/nat64/nat64_db.c b/src/plugins/nat/nat64/nat64_db.c index 61352f7457de..688fac3e3d88 100644 --- a/src/plugins/nat/nat64/nat64_db.c +++ b/src/plugins/nat/nat64/nat64_db.c @@ -122,9 +122,10 @@ nat64_db_bib_entry_create (u32 thread_index, nat64_db_t * db, kv.key[2] = bibe_key.as_u64[2]; clib_bihash_add_del_24_8 (&db->bib.out2in, &kv, 1); - fib_table_t *fib = fib_table_get (bibe->fib_index, FIB_PROTOCOL_IP6); - nat_ipfix_logging_nat64_bib (thread_index, in_addr, out_addr, proto, - in_port, out_port, fib->ft_table_id, 1); + fib_table_t *fib = fib_table_get_or_null (bibe->fib_index, FIB_PROTOCOL_IP6); + if (fib) + nat_ipfix_logging_nat64_bib (thread_index, in_addr, out_addr, proto, in_port, out_port, + fib->ft_table_id, 1); return bibe; } @@ -195,10 +196,10 @@ nat64_db_bib_entry_free (u32 thread_index, nat64_db_t * db, if (!db->addr_free) db->free_addr_port_cb (db, &bibe->out_addr, bibe->out_port, bibe->proto); - fib_table_t *fib = fib_table_get (bibe->fib_index, FIB_PROTOCOL_IP6); - nat_ipfix_logging_nat64_bib (thread_index, &bibe->in_addr, &bibe->out_addr, - bibe->proto, bibe->in_port, bibe->out_port, - fib->ft_table_id, 0); + fib_table_t *fib = fib_table_get_or_null (bibe->fib_index, FIB_PROTOCOL_IP6); + if (fib) + nat_ipfix_logging_nat64_bib (thread_index, &bibe->in_addr, &bibe->out_addr, bibe->proto, + bibe->in_port, bibe->out_port, fib->ft_table_id, 0); /* delete from pool */ pool_put (bib, bibe); @@ -435,16 +436,13 @@ nat64_db_st_entry_create (u32 thread_index, nat64_db_t * db, kv.key[5] = ste_key.as_u64[5]; clib_bihash_add_del_48_8 (&db->st.out2in, &kv, 1); - fib_table_t *fib = fib_table_get (bibe->fib_index, FIB_PROTOCOL_IP6); - nat_ipfix_logging_nat64_session (thread_index, &bibe->in_addr, - &bibe->out_addr, bibe->proto, - bibe->in_port, bibe->out_port, - &ste->in_r_addr, &ste->out_r_addr, - ste->r_port, ste->r_port, fib->ft_table_id, - 1); - nat_syslog_nat64_sadd (bibe->fib_index, &bibe->in_addr, bibe->in_port, - &bibe->out_addr, bibe->out_port, &ste->out_r_addr, - ste->r_port, bibe->proto); + fib_table_t *fib = fib_table_get_or_null (bibe->fib_index, FIB_PROTOCOL_IP6); + if (fib) + nat_ipfix_logging_nat64_session ( + thread_index, &bibe->in_addr, &bibe->out_addr, bibe->proto, bibe->in_port, bibe->out_port, + &ste->in_r_addr, &ste->out_r_addr, ste->r_port, ste->r_port, fib->ft_table_id, 1); + nat_syslog_nat64_sadd (bibe->fib_index, &bibe->in_addr, bibe->in_port, &bibe->out_addr, + bibe->out_port, &ste->out_r_addr, ste->r_port, bibe->proto); return ste; } @@ -508,16 +506,13 @@ nat64_db_st_entry_free (u32 thread_index, kv.key[5] = ste_key.as_u64[5]; clib_bihash_add_del_48_8 (&db->st.out2in, &kv, 0); - fib_table_t *fib = fib_table_get (bibe->fib_index, FIB_PROTOCOL_IP6); - nat_ipfix_logging_nat64_session (thread_index, &bibe->in_addr, - &bibe->out_addr, bibe->proto, - bibe->in_port, bibe->out_port, - &ste->in_r_addr, &ste->out_r_addr, - ste->r_port, ste->r_port, fib->ft_table_id, - 0); - nat_syslog_nat64_sdel (bibe->fib_index, &bibe->in_addr, bibe->in_port, - &bibe->out_addr, bibe->out_port, &ste->out_r_addr, - ste->r_port, bibe->proto); + fib_table_t *fib = fib_table_get_or_null (bibe->fib_index, FIB_PROTOCOL_IP6); + if (fib) + nat_ipfix_logging_nat64_session ( + thread_index, &bibe->in_addr, &bibe->out_addr, bibe->proto, bibe->in_port, bibe->out_port, + &ste->in_r_addr, &ste->out_r_addr, ste->r_port, ste->r_port, fib->ft_table_id, 0); + nat_syslog_nat64_sdel (bibe->fib_index, &bibe->in_addr, bibe->in_port, &bibe->out_addr, + bibe->out_port, &ste->out_r_addr, ste->r_port, bibe->proto); /* delete from pool */ pool_put (st, ste); From c938cbdada1b1a3233019172bcd18b54b99c5214 Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Tue, 22 Sep 2026 21:21:53 +0800 Subject: [PATCH 82/85] gha: derive only test modules that exist (#37) * gha: derive only test modules that exist The plugin rule emitted the package directory name whenever any test_*.py matched, so a change under src/plugins/nat/nat64/ produced the filter nat. The runner resolves a bare filter to the file test_nat.py and matches it literally, and no such module exists, so the job died with "No tests matched the specified filter" even though the build succeeded. The same held for acl, policer and ppp, whose names are prefixes of other test modules but have no module of their own. That gated four plugin packages on a failure unrelated to the change. Try the subdirectory component before the package name, and emit a name only when test/test_.py exists exactly, which is the discipline the src/vnet rule already used. src/plugins/nat/nat64/nat64_db.c now selects nat64 (test_nat64.py), and src/plugins/acl/acl.c selects nothing and falls back to the smoke suite. The runner's own parse_test_filter (test/run_tests.py:800) and discover_tests (test/discover_tests.py:29) confirm a bare name maps to test_.py and is matched with fnmatch against the module basename, so no emitted name can match zero tests. Type: fix * gha: TEMPORARY probe for the nat64 suite derivation Touches src/plugins/nat/nat64/ so the pull request's own diff reaches the plugin branch of the suite derivation, which is the code path this pull request changes. Removed in the next commit, before the pull request is merged. Type: ci --- .github/workflows/pr-verify.yml | 39 ++++++++++++------- src/plugins/nat/nat64/ci-derivation-probe.txt | 2 + 2 files changed, 27 insertions(+), 14 deletions(-) create mode 100644 src/plugins/nat/nat64/ci-derivation-probe.txt diff --git a/.github/workflows/pr-verify.yml b/.github/workflows/pr-verify.yml index 107765eea832..994d3b41c396 100644 --- a/.github/workflows/pr-verify.yml +++ b/.github/workflows/pr-verify.yml @@ -203,12 +203,17 @@ jobs: path: build-root/.ccache key: ccache-debug-debian13-${{ runner.arch }}-${{ github.ref_name }}-${{ github.sha }} - # Turn the changed paths into a focused TEST filter. A src/plugins/ - # change selects when a test_*.py module exists; a - # src/vnet/ change selects only when test_.py exists - # exactly, because a prefix match on a directory such as ip would pull in - # every ip4/ip6/ipip/ipsec suite. Changes under src/vppinfra, src/svm and - # src/vlib select nothing here and use the fallback smoke suite. + # Turn the changed paths into a focused TEST filter. Every emitted name + # must have an exact test_.py module, because the runner resolves a + # bare filter to that file name and matches it literally: a name with no + # such module selects no test and fails the job instead of the suite + # falling back. For src/plugins///... the subdirectory is tried + # first, so src/plugins/nat/nat64/nat64_db.c selects nat64 (test_nat64.py) + # rather than the package name nat, which has no module of its own and + # would match nothing. src/vnet/ selects only on an exact + # module too, because a prefix match on a directory such as ip would pull + # in every ip4/ip6/ipip/ipsec suite. Changes under src/vppinfra, src/svm + # and src/vlib select nothing here and use the fallback smoke suite. - name: Derive a test filter from the changed paths id: tests run: | @@ -223,23 +228,29 @@ jobs: git diff --name-only "${base}..${head}" 2>/dev/null || true) suites="" for path in ${changed}; do + name="" case "${path}" in src/plugins/*/*) - name=${path#src/plugins/} - name=${name%%/*} - name=${name//-/_} - compgen -G "test/test_${name}*.py" >/dev/null || continue + rest=${path#src/plugins/} + dir=${rest%%/*} + sub=${rest#*/} + sub=${sub%%/*} + for candidate in "${sub}" "${dir}"; do + candidate=${candidate//-/_} + if [ -f "test/test_${candidate}.py" ]; then + name=${candidate} + break + fi + done ;; src/vnet/*/*) name=${path#src/vnet/} name=${name%%/*} name=${name//-/_} - [ -f "test/test_${name}.py" ] || continue - ;; - *) - continue + [ -f "test/test_${name}.py" ] || name="" ;; esac + [ -n "${name}" ] || continue suites="${suites} ${name}" done suites=$(echo "${suites}" | tr ' ' '\n' | sed '/^$/d' | diff --git a/src/plugins/nat/nat64/ci-derivation-probe.txt b/src/plugins/nat/nat64/ci-derivation-probe.txt new file mode 100644 index 000000000000..7580f550df04 --- /dev/null +++ b/src/plugins/nat/nat64/ci-derivation-probe.txt @@ -0,0 +1,2 @@ +Temporary probe: makes this pull request touch src/plugins/nat/nat64/ so the +suite derivation for that package is exercised in CI. Removed in the next commit. From a804b1c9aefda012dc5e94847baf249b4f4fceb9 Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Wed, 23 Sep 2026 02:03:57 +0800 Subject: [PATCH 83/85] nat: drop the leftover suite-derivation probe file The throwaway src/plugins/nat/nat64/ci-derivation-probe.txt existed only so that a pull request touched src/plugins/nat/nat64/ and exercised the changed-path to suite derivation in CI. Its own text says it is removed in the next commit, and it was removed on the branch, but the pull request was squash-merged so only the state that still contained the file reached master. It is referenced by nothing and src/plugins/nat lists its sources explicitly rather than globbing them, so it has no effect on the build, but it is not part of the product and does not belong in the tree. Type: fix --- src/plugins/nat/nat64/ci-derivation-probe.txt | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 src/plugins/nat/nat64/ci-derivation-probe.txt diff --git a/src/plugins/nat/nat64/ci-derivation-probe.txt b/src/plugins/nat/nat64/ci-derivation-probe.txt deleted file mode 100644 index 7580f550df04..000000000000 --- a/src/plugins/nat/nat64/ci-derivation-probe.txt +++ /dev/null @@ -1,2 +0,0 @@ -Temporary probe: makes this pull request touch src/plugins/nat/nat64/ so the -suite derivation for that package is exercised in CI. Removed in the next commit. From 45684c416046a0d890d406bca917e4e0c3ce9254 Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Wed, 23 Sep 2026 02:05:11 +0800 Subject: [PATCH 84/85] docs: add AGENTS.md describing this VPP tree Add a working guide for AI coding agents and newcomers that know nothing about this project: what the repository is, how it is built and tested, the conventions a change is expected to follow, and the fork-specific context that differs from upstream. Two parts were corrected against the tree rather than taken as written, since both had drifted from what this fork actually contains: - The CI section described the upstream FD.io workflows, which this fork no longer has. They targeted FD.io's self-hosted Nomad runners and consumed fdio/* actions plus AWS secrets that do not exist here, so they sat queued forever; they were deleted and replaced by pr-verify.yml, which runs on GitHub-hosted runners. The section now documents the two workflows that exist, the debian:trixie container shell caveat, and the rule that the derived test filter only emits a name whose module exists. - The fork-specific section quoted a commit count beyond upstream/master, which changes on every merge. The count is removed rather than updated, so the statement does not go stale again. Type: docs --- AGENTS.md | 664 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 664 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000000..3ab84ed6734f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,664 @@ +# AGENTS.md — Working on this VPP tree + +This file is for AI coding agents (and humans) who know nothing about this +project. It describes what the repository is, how it is built, how it is +tested, and the conventions that changes are expected to follow. + +--- + +## 1. Project overview + +This is **FD.io VPP (Vector Packet Processing)**, the open-source version of +Cisco's VPP technology: a high-performance, production-quality packet +processing stack that runs on commodity CPUs. It is the dataplane framework +behind the FD.io project, licensed under **Apache-2.0**. + +VPP processes packets as a **directed graph of forwarding nodes**. Each node +receives a *frame* of buffers (vector) and processes them in a tight loop, +which enables prefetching, pipelining, and SIMD. Multiple worker threads run +replicas of the graph, with ingress-hashed packet steering. + +**This checkout is a fork.** The important remotes: + +| Remote | URL | Meaning | +| ---------- | ---------------------------- | ---------------------------------------- | +| `origin` | `git@github.com:sunoaki/vpp.git` | This fork (the one being developed here) | +| `upstream` | `git@github.com:FDio/vpp.git` | Canonical upstream FD.io VPP | + +- The current branch is `master`. +- `master` is **60 commits ahead of `upstream/master`**. These commits are + hardening/fix work (see "Fork-specific context" below), not upstream changes. +- Version string comes from `src/scripts/version` / `git describe`; as of this + writing it is `v26.10-rc0-489-gbc8310875`. +- Project metadata lives in `INFO.yaml`; the project PTL and committer list are + there and in `MAINTAINERS`. + +Upstream project resources (useful when a change may need to go upstream): +Gerrit `gerrit.fd.io` (`project=vpp`, see `.gitreview`), GitHub issues at +`https://github.com/fdio/vpp/issues`, mailing list `vpp-dev@lists.fd.io`. + +--- + +## 2. Repository layout + +| Path | Contents | +| -------------------- | ------------------------------------------------------------------------ | +| `src/vppinfra` | Core infrastructure library: vectors, pools, hashes/bihash, bitmaps, time, format/unformat, allocators, SIMD helpers | +| `src/svm` | Shared virtual memory allocation library (used for API shared memory) | +| `src/vlib` | Vector processing library: buffer & graph-node management, threads, CLI, counters, tracing, event logger | +| `src/vlibmemory` | Shared-memory / API memory management (client & server sides) | +| `src/vlibapi` | Binary API library | +| `src/vnet` | The network stack: L2/L3/L4 nodes, interfaces, FIB, features, sessions | +| `src/vpp` | The `vpp` application container (links everything), `vppctl`, `vpe.api` | +| `src/vpp-api` | API client bindings: C (`client/`), C++/C (`vapi/`), Python (`python/vpp_papi`), Lua | +| `src/vat`, `src/vat2`| API test programs / vat2 plugin framework | +| `src/vcl` | VPP Communications Library and LD_PRELOAD shim (`vppcom`, `vcl_ldpreload`) | +| `src/plugins` | ~99 bundled dataplane plugins (see below) | +| `src/drivers` | Built-in device drivers (armada, atlantic, iavf, ige, octeon) | +| `src/crypto_engines` | Crypto backends: `ipsecmb`, `native`, `openssl` | +| `src/tools` | `vppapigen` (API compiler), `g2`, `perftool`, `appimage` | +| `src/scripts` | Version/utility scripts, including `src/scripts/version` | +| `src/cmake` | CMake rule/function definitions and `platform/*.cmake` CPU tuning files | +| `test` | Python unit/functional test harness (`VppTestCase`) + `test/asf` + `test/hs-test` | +| `test/hs-test` | Go/Ginkgo host-stack end-to-end test framework (Docker topologies) | +| `extras` | Packaging (`deb`, `rpm`, `snap`), Vagrant, emacs plugin generator, libmemif, config tools, kube-test | +| `docs` | Sphinx documentation tree (`.rst`/`.md`, symlinks into `src/`) | +| `build-data` | Build metadata: `packages/*.mk`, `platforms/*.mk` | +| `build-root` | Build output directory (generated; largely gitignored) | +| `build`, `build-data`, `build-root` | See build system section | + +`README.md` documents the directory layout in the project's own words. +`MAINTAINERS` is authoritative for who owns a component and for the feature-id +vocabulary used by commit-message checks. + +### The plugins directory + +`src/plugins` contains ~99 subdirectories, each a shared object +(`_plugin.so`) built from its own `CMakeLists.txt`. There is no central +registration list: `src/plugins/CMakeLists.txt` globs `*/CMakeLists.txt` and +adds each subdirectory, and each plugin calls `add_vpp_plugin( ...)`. +Plugin selection can be constrained at configure time with +`-DVPP_PLUGINS=` or `-DVPP_EXCLUDED_PLUGINS=`. + +Representative plugins: `acl`, `nat`, `ipsec`, `dpdk`, `rdma`, `memif`, +`linux-cp`, `gre`, `quic`, `http_static`, `srv6-*`, `lisp`, `flowprobe`, +`sfdp*`, `lacp`, `bonding`, `pppoe`, `gtpu`, `map`, `nsh`, `vhost`, +`af_packet`, `af_xdp`, `netmap`. + +Note that not every protocol/feature is a plugin: `session`, `tls`, `tcp`, +`udp`, `vxlan`, `ipsec`, `fib`, `ip`, `ip6` and others live in `src/vnet`, +while `src/plugins` holds the separately-loadable `.so` modules. + +--- + +## 3. Technology stack + +- **Language:** C (C11-style, GNU extensions). Some C++ in `src/vpp-api/vapi` + (`vapi.hpp`, `vapi_cpp_test.cpp`) and `src/plugins/*/` C++ usages. +- **Test/aux languages:** Python 3 (functional test harness, docs, tooling), + Go (hs-test, kube-test), shell. +- **Build system:** top-level GNU `Makefile` → `build-root/Makefile` → + **CMake (>= 3.19) generating Ninja** build files. The legacy autotools-style + build is gone; `make bootstrap` is a no-op. +- **Compilers:** Clang (preferred/default when `CC=cc`) or GCC, both **>= 9.0.0**. + The build is compiled with `-Werror` in practice, so warnings are failures. +- **External dependencies:** built into `build/external` (DPDK `26.07`, + rdma-core `64.0`, ipsec-mb, quicly, libcbor, xdp-tools, daq, octeon-roc). + `build/optional` holds optional deps. See `build/external/packages/*.mk`. +- **Docs:** Sphinx (`docs/`, `docs/Makefile`, `docs/conf.py`), reStructuredText + and Markdown, with `sphinxcontrib-spelling`. +- **Multi-architecture:** performance-critical nodes are compiled once per CPU + variant (e.g. `x86_64_v3`, `avx2`, `avx512`, `neoverse-*`), and the best + variant is selected at runtime. Platform tuning files live in + `src/cmake/platform/` (`icelake-server`, `sapphirerapids`, `emeraldrapids`, + `neoverse-n1/n2/v2`, `octeon9/10`, `cn913x`). + +--- + +## 4. Build system and commands + +All commands are run from the repository root. `make` with no target prints +the target list (`make help` is the same). + +### One-time setup + +```bash +make install-dep # install distro build dependencies (sudo) +make install-ext-deps # build/install DPDK, rdma-core, etc. into build/external +make install-opt-deps # optional dependencies (not always needed) +``` + +`make install-dep` also runs +`git config commit.template .git_commit_template.txt`. + +### Build + +```bash +make build # debug binaries -> build-root/build-vpp_debug-native +make build-release # release binaries -> build-root/build-vpp-native +make rebuild # wipe + build (debug) +make rebuild-release # wipe + build (release) +make wipe # remove debug build products +``` + +Outputs land in `build-root/`: +`build-vpp-native/` (release), `build-vpp_debug-native/` (debug), +`build-vpp_gcov-native/` (coverage), and matching `install-vpp-*` trees. + +### Incremental C compilation (much faster than a full `make build`) + +The CMake/Ninja tree is directly usable: + +```bash +ninja -C build-root/build-vpp_debug-native/vpp +# e.g. a single plugin: +ninja -C build-root/build-vpp_debug-native/vpp lib/x86_64-linux-gnu/vpp_plugins/gre_plugin.so +``` + +`make compdb` regenerates `compile_commands.json` at the repo root (used by +clangd/IDEs; gitignored). + +### Run / debug the binary + +```bash +make run # run debug vpp (needs sudo for hugepages/NIC access) +make run-release # run release vpp +make debug # run debug vpp under gdb +make debug-release # run release vpp under gdb +make run-vat # run the vpp-api-test tool +``` + +Useful make arguments: `V=1` (verbose build), `STARTUP_CONF=`, +`STARTUP_DIR=`, `GDB=`, `PLATFORM=`, `DPDK_CONFIG=<...>` +(e.g. `"no-pci"`), `SAMPLE_PLUGIN=yes`, `DISABLED_PLUGINS=`. +`src/vpp/conf/startup.conf` is the reference startup configuration +(`unix`, `api-trace`, `api-segment`, `socksvr`, `cpu`, `dpdk`, ... stanzas). + +### Alternative: the `configure` script + +`./configure` is an experimental, standalone CMake driver for out-of-tree or +cross builds (`--build-type`, `--platform`, `--arch`, `--plugins`, `--drivers`, +`--crypto-engines`, `--minimal`, `--native-only`, `--sanitize mem`, ...). +Consult its `--help` before changing it; the comment asks to check with the +maintainer first. + +### Sanitizers + +CMake option `-DVPP_ENABLE_SANITIZE_ADDR=ON` enables AddressSanitizer; the +default ASAN options are set in `src/CMakeLists.txt`. The test harness exports +`ASAN_OPTIONS` from `test/Makefile`. + +--- + +## 5. Code organization and conventions + +### Layer boundaries + +`vppinfra` → `svm` → `vlib` → `vlibmemory`/`vlibapi` → `vnet` → `vpp`. +Keep dependencies pointing downward. `src/CMakeLists.txt` lists `SUBDIRS` in +that order ("order matters"). Plugins link against the core libraries; core +code must not depend on plugins. + +### Plugin structure (the dominant unit of new code) + +A plugin directory typically contains: + +``` +CMakeLists.txt # add_vpp_plugin(...) +plugin.c # VLIB_PLUGIN_REGISTER() { .version = VPP_BUILD_VER, .description = ... } +FEATURE.yaml # machine-readable feature metadata (required, schema-validated) +.api # binary API definitions (if the plugin has an API) +_api.c # API message handlers (generated header + manual handlers) +.h # plugin-internal header +node.c # graph node dispatch functions +.c # main/init/CLI +error.def # error-string definitions, included via foreach__error +``` + +Example — `src/plugins/gre/CMakeLists.txt`: + +```cmake +add_vpp_plugin(gre + SOURCES gre.c node.c gre_api.c interface.c pg.c plugin.c + MULTIARCH_SOURCES node.c gre.c + INSTALL_HEADERS gre.h error.def + API_FILES gre.api +) +``` + +`add_vpp_plugin` (defined in `src/cmake/plugin.cmake`) supports: +`SOURCES`, `MULTIARCH_SOURCES`, `MULTIARCH_FORCE_ON`, `API_FILES`, +`API_TEST_SOURCES`, `VAT_AUTO_TEST`, `INSTALL_HEADERS`, `LINK_LIBRARIES`, +`LINK_FLAGS`, `COMPONENT`, `DEV_COMPONENT`, `SUPPORTED_OS_LIST`. +It installs the plugin to `/vpp_plugins` and any API test plugin to +`vpp_api_test_plugins` / `vat2_plugins`. + +A generator exists for new plugins: +`cd src/plugins && ../../extras/emacs/make-plugin.sh`. After adding a plugin +directory you generally just rebuild; no other file needs editing (the glob +picks it up). See `docs/developer/plugindoc/add_plugin.rst`. + +### Feature metadata (`FEATURE.yaml`) + +Every plugin/component should ship a `FEATURE.yaml` (105 exist). It is +validated against a JSON schema by `extras/scripts/fts.py`: + +- `name`, `description`, `features` (list), `state` ∈ + {production, experimental, development}, + `properties` ⊆ {API, CLI, STATS, MULTITHREAD}, `maintainer` (string or list). + +Validate with `make checkfeaturelist`; dump the list with `make featurelist`. + +### Multi-architecture nodes + +Follow `docs/developer/corearchitecture/multiarch/nodefns.rst`: + +- Declare the node function with `VLIB_NODE_FN ()`, and the function + name **must** match the graph node name. +- Bracket the single copies of `VLIB_REGISTER_NODE`, error strings, and trace + format functions in `#ifndef CLIB_MARCH_VARIANT ... #endif`. +- **Never** set the `.function` member of `VLIB_REGISTER_NODE`; the multiarch + constructor selects it at runtime. +- Mark the body inline with `always_inline` (not `static inline`), or the + compiler will not emit per-variant copies. +- List the file under `MULTIARCH_SOURCES` in `CMakeLists.txt`. + +### Binary API (`.api` files) + +The API is defined in a custom IDL and compiled by `src/tools/vppapigen`. See +`docs/interfacing/binapi/vpp_api_language.rst`. Key points: + +- `option version = "X.Y.Z";` at the top of each `.api` file. +- Request/reply naming conventions: `` / `_reply`; + dump/detail: `_dump` / `_details`; events: `want_*` + + `service { rpc ... events ... }`. +- Every request carries `u32 client_index; u32 context;`. +- `autoreply` is shorthand for a reply containing only `retval`. +- API message CRCs are part of the wire contract. `make checkstyle-api` + (`extras/scripts/crcchecker.py --check-patchset`) rejects incompatible + changes to APIs with semantic version >= 1.0.0. If an API change is + intentional, the CRC must be deliberately updated and the change discussed + (the checker's failure message points at the API change process). +- Generated JSON/header artifacts are produced by + `make json-api-files`, `make go-api-files`. + +### License headers + +New files must contain an `SPDX-License-Identifier:` line (checked by +`make checkstyle` for added `*.c`, `*.h`, `*.cmake`, `*.py`, `CMakeLists.txt`). +The tree is migrating from the long Apache-2.0 boilerplate to the short SPDX +form (thousands of files already use `/* SPDX-License-Identifier: Apache-2.0 +* Copyright (c) ... */`). New code should use the SPDX form. + +### Coding style (C) + +- `.clang-format` is authoritative: **GNU base style, tabs (`UseTab: Always`), + column limit 100, 2-space continuation indent**, `SpaceAfterCStyleCast: true`, + includes not sorted. Minimum `clang-format` version is 11. +- `.clang-tidy` enables `misc-*`, `bugprone-*` (minus swappable-params and + reserved-identifier), `performance-*`, `clang-analyzer-*` (minus + `valist.Uninitialized`). +- `make checkstyle` (via `extras/scripts/checkstyle.sh`) checks the **most + recent commit** (`CHECKSTYLE_COMMIT=HEAD~N` to override) and fails on: + trailing whitespace, Emacs indentation-control comments on modified lines, + missing SPDX in new files, the deprecated + `fd.io coding-style-patch-verification` footer, and clang-format diffs. +- `make fixstyle` applies clang-format to the current commit's diff. +- The expected clang-format major version is set per OS in the top-level + `Makefile` (`CLANG_FORMAT_VER`, e.g. 11, 15, 19). A mismatch makes + `checkstyle` fail with a version error. +- Avoid Emacs indentation-control comments in new or modified code. Note that + `checkstyle.sh` detects them by grepping added lines for the literal marker, + so even a comment *quoting* the marker trips the check. + +### Python style + +Python under `test/` (and repo-wide) is formatted with **black**, target +`py39`: `make checkstyle-python` / `make fixstyle-python`. The test harness +installs its own pinned toolchain in a virtualenv. + +### Go style + +`test/hs-test` and `extras/kube-test` are checked with `goimports` +(`v0.39.0`): `make checkstyle-go` / `make fixstyle-go`. Go modules are +`fd.io/hs-test` (go 1.26.5) and the kube-test module. + +### Commit messages (enforced) + +Format is described in `.git_commit_template.txt` and enforced by +`extras/scripts/check_commit_msg.sh` (`make checkstyle-commit`): + +- Subject: `: `, max ~50 chars, lowercase, imperative, + no trailing period. `feature-name` **must be a feature id present in + `MAINTAINERS`** (the `I:` lines). Commits spanning multiple components + should be split. +- Body explains *why*, wrapped ~72 chars, with a `Type:` line whose value is + one of: `feature`, `fix`, `refactor`, `improvement`, `style`, `docs`, + `test`, `make`, `ci`. +- Optional `Ticket:`, `Fixes:`, `Change-Id:`, `Signed-off-by:` lines. + +Example subject lines from this tree: `linux-cp: drop nexthops on deleted +interfaces`, `fib: guard attached export against bad fib index`, +`build: make deb version vary per commit`. + +### Documentation style + +Docs live as `.rst`/`.md` next to the code in `src/` (and `test/`, `extras/`) +and are **symlinked** into `docs/`. `extras/scripts/check_documentation.sh` +verifies that every doc file is either linked from `docs/` or listed in +`docs/docsignore`. Build with `make docs`, clean with `make docs-clean`, spell +check with `make docs-spell`; output is `build-root/docs/html/index.html`. + +--- + +## 6. Testing + +There are three test systems. "Over 1,000 unit test vectors" are referenced in +`docs/developer/build-run-debug/testing_vpp.rst`. + +### 6.1 Python functional tests (`test/`) — the primary harness + +Built on Python `unittest` + **Scapy** for packet construction/inspection. + +- Base classes: `test/framework.py::VppTestCase` (packet-generator/scapy tests) + and `test/asfframework.py::VppAsfTestCase` ("A Scapy Free" tests under + `test/asf/`, which use VPP's own packet generator without Scapy). +- Lifecycle: `setUpClass` → `setUp` → `test_` → `tearDown` → + `tearDownClass`. Each test class gets its own temporary directory + (`/tmp/vpp-unittest--XXXXXX`) and shared-memory prefix, so tests + are isolated; each directory holds `log.txt`, `pg*_in.pcap`, `pg*_out.pcap`. +- Object model helpers live in `test/vpp_*.py` (e.g. `vpp_gre_interface.py`, + `vpp_ip_route.py`); assertions use `unittest` assert methods plus + `self.statistics` counters. +- Dependencies are pinned: `test/requirements.txt` (source) is compiled into + `test/requirements-3.txt` (hash-pinned lockfile). The venv is created at + `build-root/test/venv`, using `uv` when available and falling back to + pip/pip-tools. `test/patches/scapy-/*.patch` are applied to Scapy + after install. +- Tests can run in parallel; each suite is a forked process. + +Commands (run from the repo root): + +```bash +make test # build vpp + run basic functional tests +make test-debug # same, against the debug image +make test-all # basic + extended tests (EXTENDED_TESTS=1) +make retest # run tests without rebuilding vpp +make TEST=test_gre test # one file +make TEST=gre test # 'test_' prefix omitted +make TEST='gre.TestGRE.test_gre' test # file.class.method, wildcards allowed +make TEST_JOBS=auto test # parallel processes +make test-list TEST=... # show which tests the filter selects, no run +make test-help # full list of test targets and variables +make test-shell # interactive shell with the test env activated +make test-start-vpp-in-gdb # start a vpp for DEBUG=attach +make test-wipe # clean temp files/venvs +``` + +Important test variables: `V=[0|1|2]` (verbosity), `TEST_JOBS=[n|auto]`, +`MAX_VPP_CPUS`, `FAILFAST`, `TIMEOUT` (default 600s), `RETRIES`, `STEP`, +`SANITY`, `EXTENDED_TESTS`, `VARIANT=` (test a specific multiarch +variant, e.g. `skx`), `RND_SEED`, `CACHE_OUTPUT`, `USE_SMT`, `SKIP_TESTS`, +`DEBUG=`. + +Failed tests are copied to `/tmp/vpp-failed-unittests/` and, in CI, archived. + +When adding a test for a feature, look for an existing `test/test_.py` +and mirror its structure; plugin tests commonly skip themselves when the +plugin is excluded (`config.excluded_plugins`). + +### 6.2 Host-stack end-to-end tests (`test/hs-test`) — Go + Ginkgo + Docker + +End-to-end tests for the host stack that need multiple VPP instances, network +namespaces, and external tools. Written in Go with Ginkgo; topology is built +with Docker and `ip`. Requires root and a sane `kernel.core_pattern` (no pipe, +otherwise core dumps are not detected). See `test/hs-test/README.rst`. + +```bash +make -C test/hs-test build # build test infra / docker images +make -C test/hs-test test # run tests +make -C test/hs-test test-debug # run against debug VPP image +make -C test/hs-test list-tests +make -C test/hs-test test TEST=MyTest +make -C test/hs-test test PARALLEL=4 PERSIST=true V=2 +make cleanup-hst # remove containers/namespaces from last run +``` + +Suites are grouped by topology and run in parallel; `RUN_ID` isolates +concurrent runs on one checkout. + +### 6.3 C unit tests + +- `src/vppinfra/test/test_*.c` are standalone C unit tests, built only when + `-DVPP_BUILD_VPPINFRA_TESTS=ON`. They are registered via `add_vpp_test` in + `src/vppinfra/CMakeLists.txt`. +- Individual components have in-tree tests, e.g. `src/vnet/interface_test.c`, + `src/vpp-api/client/test.c`, and per-plugin API test plugins + (`API_TEST_SOURCES`). +- `src/tools/vppapigen/test_vppapigen.py` tests the API generator and is run by + `make verify`. + +### 6.4 Coverage and "make verify" + +```bash +make test-cov # gcov build + tests + lcov HTML report +make test-cov-both # merge Python + Go (hs-test) coverage +make test-cov-build / test-cov-prep / test-cov-post # phased coverage +make test-cov-hs # host-stack tests with coverage +make cov-merge # merge + genhtml the lcov traces +``` + +Report output: `build-root/test-coverage-merged/html/index.html`. Details in +`docs/developer/build-run-debug/code_coverage.rst`. + +`make verify` runs `pkg-verify` (build packages + sample-plugin + libmemif), +then `vppapigen` tests and `make test` — only on Ubuntu 22.04 +(`MAKE_VERIFY_GATE_OS`). + +### 6.5 CI + +CI is GitHub Actions under `.github/workflows/`. This fork keeps exactly two +workflows, both on GitHub-hosted runners: + +- `pr-verify.yml` — runs on `pull_request` against `master`. Two jobs: + - `style` (no build): `make checkfeaturelist`, `make checkstyle` over the + whole pull request, `make checkstyle-api`, `make checkstyle-python`, and + `extras/scripts/check_commit_msg.sh` for every non-merge commit in the PR. + - `build and test`: installs dependencies, builds VPP debug with `-Werror` + under ccache, derives a focused `TEST=` filter from the changed paths + (falling back to the `ip4` smoke suite), and runs `make test-debug`. +- `build-deb-trixie.yml` — builds Debian 13 (Trixie) DEB packages on push to + `master` (and can publish a GitHub Release). + +Both jobs run inside a `debian:trixie` container, which matters for the shell: +a container job gets `sh` (dash in this image) for `run:` steps unless the job +sets `defaults: run: shell: bash`, and dash rejects the bash parameter +expansions used by the test-filter step. + +The upstream FD.io workflows are **not** present here. They targeted FD.io's +self-hosted Nomad runners and consumed `fdio/*` actions plus AWS secrets, none +of which exist on this fork, so they sat queued forever; they were deleted. +Do not reintroduce a `self-hosted` runner target, an `fdio/` action, or an AWS +secret into a workflow. + +The test filter only ever emits a name for which `test/test_.py` exists +exactly, because the runner resolves a bare filter to that file name and +matches it literally — a name with no module selects nothing and fails the job +instead of falling back. For `src/plugins///...` the subdirectory is +tried first, so `src/plugins/nat/nat64/nat64_db.c` selects `nat64`. + +Reusable composite actions still live in `.github/actions/` (`vpp-build`, +`vpp-make-test`, `vpp-install-deps`, `vpp-install-ext-deps`, +`vpp-docker-runtime-setup`), but the two workflows above do not currently use +them. + +Dependabot is configured for `github-actions` only, with PRs disabled +(`open-pull-requests-limit: 0`). + +Note that `lsp_diagnostics` and similar tools that resolve paths against their +own working directory cannot see this repository, so compilation and formatting +are verified by CI and by `extras/scripts/checkstyle.sh` run locally with the +pinned `CLANG_FORMAT_VER=19`, which is hunk-scoped. + +Run the local equivalents of the CI gates before considering a change done: + +```bash +make checkstyle # C formatting + feature list +make checkstyle-commit # commit message (feature id + Type:) +make checkstyle-python # black +make checkstyle-api # API CRC compatibility +make checkstyle-go # goimports +make test-checkstyle # Python + Go test-code style +make docs-spell # Sphinx spelling +``` + +--- + +## 7. Security considerations + +The project has an explicit security policy in `SECURITY.md`; the summary below +is not a substitute for reading it. + +- **Report vulnerabilities privately** to `security@lists.fd.io`. Do not open a + public issue for a security bug. The full process is in + `docs/aboutvpp/security/tsc_vulnerability_management_v2.rst`; past advisories + are in `docs/aboutvpp/security/security_advisories.rst`. +- **What counts as a security bug:** a bug exploitable through the + **dataplane** — i.e. triggered by crafted packets arriving on an interface + and processed by the forwarding graph, with no prior privileged access. + Examples: a crafted packet that crashes VPP (DoS) or achieves RCE; any + memory-safety flaw reachable from normal packet processing. +- **What does NOT count:** bugs reachable only through the **binary API** or + the debug CLI (`vppctl`). Those interfaces are *trusted* — the caller is + assumed to be in the same administrative trust domain as VPP. Such bugs + should still be fixed, but through the normal public bug process. +- Concretely for agents: when auditing or modifying dataplane code, treat + packet-derived lengths/indices/offsets as untrusted and validate them. When a + crash requires an API/CLI precondition, it is a regular bug, not a CVE. +- The debug CLI (`vppctl`) and the API socket are privileged interfaces: + `unix { cli-listen /run/vpp/cli.sock ... }` in the startup config controls + access, and the `api-segment`/`unix` stanzas set the `gid` used to restrict + socket permissions. +- VPP runs as a privileged process using hugepages and (for DPDK) kernel + drivers such as `uio_pci_generic`; see `docs/gettingstarted/running/`. + +--- + +## 8. Packaging and deployment + +```bash +make pkg-deb # build DEB packages +make pkg-deb-debug # debug DEB packages +make pkg-rpm # build RPM packages (depends on `make dist`) +make pkg-srpm # source RPM +make pkg-snap # build SNAP package +make pkg-verify # full package build: vpp + sample-plugin + libmemif + pkg-$(PKG) +``` + +- Package flavor is auto-detected from `/etc/os-release`: `deb` (Ubuntu, Debian, + LinuxMint), `rpm` (RHEL, CentOS, Fedora, openSUSE-Leap, Rocky, AlmaLinux, + Anolis, Kylin), `pkg` (FreeBSD). +- `make dist` produces `build-root/vpp-.tar.xz`. +- Debian packaging metadata is under `build-root/deb/debian/` and + `extras/deb/`; RPM under `extras/rpm/`; snap under `extras/snap/`. +- On install, VPP creates a `vpp` user/group, a systemd unit + (`vpp.service`), hugepage sysctls (`/etc/sysctl.d/80-vpp.conf`), and a + default `/etc/vpp/startup.conf`. Add users who need `vppctl` to the `vpp` + group (`usermod -a -G vpp `). See `docs/gettingstarted/running/`. +- `extras/vagrant/build.sh` bootstraps dependencies, builds and installs VPP on + a Linux host; `extras/vagrant/` has a Vagrant VM for development. +- `extras/vpp_config/` is a Python tool for generating `startup.conf` for a + given set of NICs; `extras/scripts/` contains operational helpers + (`pci-nic-bind`, `lsnet`, `vfctl`, `vpp-review`, ...). + +Version numbering: `src/scripts/version` derives the version from +`git describe --long --match "v*"`, with fallbacks for repos without tags. In +this fork the deb workflow synthesises versions of the form +`0.0.0-release+~g` so that rebuilt packages sort strictly above +earlier builds (see commits `7ee6487a3` and `bc8310875`); this matters because +an unchanged version string makes apt skip the new package and can leave +mismatched shared libraries installed. + +--- + +## 9. Fork-specific context (important for this checkout) + +The commits on `master` beyond `upstream/master` are largely **defensive +hardening and crash fixes** to dataplane and control-plane paths, plus build/CI +work. Areas touched: `linux-cp` (netlink error recovery, interface liveness +validation), `fib` (bad index guards, attached-export), `gre`, `teib`, +`vlib` buffer/handoff cleanup, `svm` mapping/fifo rollback, `session` fifo +migration, `rdma`, `cnat`, `nat`, `memif`, `flowprobe`, `dpdk` (incl. Huawei +Hi1822/hinic PMD handling), `vhost`, `tls`, `timer`/`tw_timer`, `vapi` message +size bounds, `http`/`http_static` parsing, `iavf` TX placeholders, `sfdp`, and +`.github/workflows` (the unrunnable FD.io workflows removed, a GitHub-hosted +pull-request verification workflow added, Debian 13 DEB build kept). + +Practical implications: + +- Fixes here often validate indices/handles derived from external input + (netlink, API, packets) before use. Match that style: prefer explicit + validation and a graceful skip/drop over dereferencing. +- The tree is kept mergeable with upstream. Prefer minimal, upstreamable + changes; avoid introducing fork-only abstractions into core code. +- Commit subjects still use upstream feature ids from `MAINTAINERS`, and + `Type: fix` is used on bug fixes. +- `.omo/` and `.sisyphus/` are **local agent workspace metadata** + (`run-continuation/`, handoff notes, patches). They are gitignored + (`.gitignore`: "Agent workspace metadata") and are not part of the product — + do not treat their contents as project documentation or commit them. +- `.codegraph` is a symlink to a local code-index cache; also not part of the + project. + +--- + +## 10. Quick reference + +```bash +# Build +make install-dep && make install-ext-deps +make build # debug +make build-release # release +ninja -C build-root/build-vpp_debug-native/vpp # incremental + +# Run +make run # debug binary +make debug # debug under gdb + +# Test +make test # python functional tests +make TEST=gre test # filtered +make TEST_JOBS=auto test # parallel +make test-all # + extended +make -C test/hs-test test # host-stack e2e (Go/Ginkgo) +make test-cov # coverage report +make test-help # all options + +# Style / gates +make checkstyle # clang-format on last commit +make fixstyle # auto-format last commit +make checkstyle-commit # commit message +make checkstyle-python # black +make checkstyle-api # API CRC compatibility +make checkstyle-go # goimports +make checkfeaturelist # FEATURE.yaml schema + +# Docs +make docs # -> build-root/docs/html/index.html +make docs-clean +``` + +Ground rules for agents working in this tree: + +1. Read `MAINTAINERS` for the owning maintainer and the correct feature id + before writing a commit message. +2. Keep changes scoped to one component per commit (`feature-name:` prefix). +3. New C files get an `SPDX-License-Identifier` header; formatting is + clang-format with tabs and a 100-column limit. +4. Treat packet-derived data as untrusted; validate indices, lengths, and + handles before use. +5. Add or extend tests under `test/` (Python) or `test/hs-test` (Go) when + behavior changes, and run the relevant `make test TEST=...` and style + gates before finishing. +6. Update `FEATURE.yaml` and the `.api` version/CRC when a plugin's feature set + or API changes. From 7032538a165bbfd0eab55e7443ee0b9e0d6e2242 Mon Sep 17 00:00:00 2001 From: Bo Xu Date: Wed, 23 Sep 2026 03:56:41 +0800 Subject: [PATCH 85/85] vpp: bound IAC handling by rx_buf_len in vppctl process_input() only checked i < rx_buf_len before treating the byte at i as the start of a telnet IAC negotiation, but then read rx_buf[i + 1] and rx_buf[i + 2] unconditionally. When an IAC byte is the last byte filled by recv(), those reads run 1-2 bytes past the stack buffer. vppctl is an app-side client, so an unusual or hostile server can drive the out-of-bounds read by sending a truncated IAC sequence at the end of a response. Break out of the loop when fewer than two bytes remain after i, so a truncated negotiation at the end of the buffer is not decoded. Well-formed input still has i + 2 < rx_buf_len and is handled exactly as before. Type: fix --- src/vpp/app/vppctl.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/vpp/app/vppctl.c b/src/vpp/app/vppctl.c index 6a981d69b67d..8ce574ef3dac 100644 --- a/src/vpp/app/vppctl.c +++ b/src/vpp/app/vppctl.c @@ -112,6 +112,9 @@ process_input (int sock_fd, unsigned char *rx_buf, int rx_buf_len, { if (rx_buf[i] == IAC) { + /* a truncated IAC negotiation needs 2 bytes past i; don't overrun */ + if (i + 2 >= rx_buf_len) + break; if (rx_buf[i + 1] == SB) { char opt = rx_buf[i + 2];