diff --git a/.clang-format b/.clang-format index 0d945dd697..5f5b5f566e 100644 --- a/.clang-format +++ b/.clang-format @@ -1,4 +1,4 @@ -# using [clang-formt-12 options](https://releases.llvm.org/12.0.0/tools/clang/docs/ClangFormatStyleOptions.html) +# using [clang-format-21 options](https://releases.llvm.org/21.1.0/tools/clang/docs/ClangFormatStyleOptions.html) RawStringFormats: - Language: Cpp Delimiters: diff --git a/.github/actions/install-wasi-sdk-wabt/action.yml b/.github/actions/install-wasi-sdk-wabt/action.yml index 31d22c18d5..16949aae82 100644 --- a/.github/actions/install-wasi-sdk-wabt/action.yml +++ b/.github/actions/install-wasi-sdk-wabt/action.yml @@ -15,6 +15,13 @@ inputs: os: description: "Operating system to install on (ubuntu, macos)" required: true + # Default 29 because addr2line.py (samples/debug-tools*) requires + # llvm-symbolizer / llvm-addr2line, which only ship in wasi-sdk 29+. + # Callers can pin a different version when they have a specific need. + wasi_sdk_version: + description: "wasi-sdk major version to install (e.g. '25', '29', '33')" + required: false + default: "29" runs: using: "composite" @@ -29,13 +36,17 @@ runs: - name: Set up wasi-sdk and wabt on Ubuntu if: ${{ startsWith(inputs.os, 'ubuntu') }} shell: bash + env: + SDK_VER: ${{ inputs.wasi_sdk_version }} run: | + SDK_DIR="wasi-sdk-${SDK_VER}.0-x86_64-linux" echo "Downloading wasi-sdk for Ubuntu..." - sudo wget -O wasi-sdk.tar.gz --progress=dot:giga https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-25/wasi-sdk-25.0-x86_64-linux.tar.gz + sudo wget -O wasi-sdk.tar.gz --progress=dot:giga \ + "https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-${SDK_VER}/${SDK_DIR}.tar.gz" echo "Extracting wasi-sdk..." sudo tar -xf wasi-sdk.tar.gz - sudo ln -sf wasi-sdk-25.0-x86_64-linux/ wasi-sdk + sudo ln -sf "${SDK_DIR}/" wasi-sdk echo "Downloading wabt for Ubuntu..." sudo wget -O wabt.tar.gz --progress=dot:giga https://github.com/WebAssembly/wabt/releases/download/1.0.37/wabt-1.0.37-ubuntu-20.04.tar.gz @@ -47,19 +58,23 @@ runs: /opt/wasi-sdk/bin/clang --version /opt/wabt/bin/wasm-interp --version - echo "::notice::wasi-sdk-25 and wabt-1.0.37 installed on ubuntu" + echo "::notice::wasi-sdk-${SDK_VER} and wabt-1.0.37 installed on ubuntu" working-directory: /opt - name: Set up wasi-sdk and wabt on macOS on Intel if: ${{ inputs.os == 'macos-15-intel' }} shell: bash + env: + SDK_VER: ${{ inputs.wasi_sdk_version }} run: | + SDK_DIR="wasi-sdk-${SDK_VER}.0-x86_64-macos" echo "Downloading wasi-sdk for macOS on Intel..." - sudo wget -O wasi-sdk.tar.gz --progress=dot:giga https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-25/wasi-sdk-25.0-x86_64-macos.tar.gz + sudo wget -O wasi-sdk.tar.gz --progress=dot:giga \ + "https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-${SDK_VER}/${SDK_DIR}.tar.gz" echo "Extracting wasi-sdk..." sudo tar -xf wasi-sdk.tar.gz - sudo ln -sf wasi-sdk-25.0-x86_64-macos wasi-sdk + sudo ln -sf "${SDK_DIR}" wasi-sdk echo "Downloading wabt for macOS on Intel..." sudo wget -O wabt.tar.gz --progress=dot:giga https://github.com/WebAssembly/wabt/releases/download/1.0.36/wabt-1.0.36-macos-12.tar.gz @@ -71,19 +86,23 @@ runs: /opt/wasi-sdk/bin/clang --version /opt/wabt/bin/wasm-interp --version - echo "::notice::wasi-sdk-25 and wabt-1.0.36 installed on ${{ inputs.os }}" + echo "::notice::wasi-sdk-${SDK_VER} and wabt-1.0.36 installed on ${{ inputs.os }}" working-directory: /opt - name: Set up wasi-sdk and wabt on macOS on ARM if: ${{ inputs.os == 'macos-15' }} shell: bash + env: + SDK_VER: ${{ inputs.wasi_sdk_version }} run: | + SDK_DIR="wasi-sdk-${SDK_VER}.0-arm64-macos" echo "Downloading wasi-sdk for macOS on ARM..." - sudo wget -O wasi-sdk.tar.gz --progress=dot:giga https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-25/wasi-sdk-25.0-arm64-macos.tar.gz + sudo wget -O wasi-sdk.tar.gz --progress=dot:giga \ + "https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-${SDK_VER}/${SDK_DIR}.tar.gz" echo "Extracting wasi-sdk..." sudo tar -xf wasi-sdk.tar.gz - sudo ln -sf wasi-sdk-25.0-arm64-macos wasi-sdk + sudo ln -sf "${SDK_DIR}" wasi-sdk echo "Downloading wabt for macOS on ARM..." sudo wget -O wabt.tar.gz --progress=dot:giga https://github.com/WebAssembly/wabt/releases/download/1.0.37/wabt-1.0.37-macos-14.tar.gz @@ -95,12 +114,14 @@ runs: /opt/wasi-sdk/bin/clang --version /opt/wabt/bin/wasm-interp --version - echo "::notice::wasi-sdk-25 and wabt-1.0.37 installed on ${{ inputs.os }}" + echo "::notice::wasi-sdk-${SDK_VER} and wabt-1.0.37 installed on ${{ inputs.os }}" working-directory: /opt - name: Set up wasi-sdk and wabt on Windows if: ${{ startsWith(inputs.os, 'windows') }} shell: bash + env: + SDK_VER: ${{ inputs.wasi_sdk_version }} run: | choco install -y wget @@ -108,7 +129,8 @@ runs: mkdir -p /opt/wabt echo "Downloading wasi-sdk for Windows..." - wget -O wasi-sdk.tar.gz --progress=dot:giga https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-25/wasi-sdk-25.0-x86_64-windows.tar.gz + wget -O wasi-sdk.tar.gz --progress=dot:giga \ + "https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-${SDK_VER}/wasi-sdk-${SDK_VER}.0-x86_64-windows.tar.gz" echo "Extracting wasi-sdk..." tar --strip-components=1 -xf wasi-sdk.tar.gz -C /opt/wasi-sdk @@ -122,4 +144,4 @@ runs: /opt/wasi-sdk/bin/clang --version /opt/wabt/bin/wasm-interp --version - echo "::notice::wasi-sdk-25 and wabt-1.0.37 installed on Windows" + echo "::notice::wasi-sdk-${SDK_VER} and wabt-1.0.37 installed on Windows" diff --git a/.github/scripts/codeql_buildscript.sh b/.github/scripts/codeql_buildscript.sh index 99060af8f4..dfc5faef34 100755 --- a/.github/scripts/codeql_buildscript.sh +++ b/.github/scripts/codeql_buildscript.sh @@ -19,9 +19,20 @@ sudo wget --progress=dot:giga -O clang+llvm-x86_64-linux-gnu.tar.xz https://gith popd # libtinfo.so.5 for /opt/llvm-18.1.8/lib/libomptarget.rtl.amdgpu.so.18.1 +# Install the libtinfo5 compat package at the same ncurses version as the +# runner's already-installed libtinfo6; fall back to the newest one in the +# pool. A hard-coded URL 404s once the ncurses point release is bumped, which +# then breaks the wamrc link against libomptarget (NCURSES_TINFO_5 symbols). sudo apt -qq update -wget http://security.ubuntu.com/ubuntu/pool/universe/n/ncurses/libtinfo5_6.3-2ubuntu0.1_amd64.deb -sudo apt install -y -qq ./libtinfo5_6.3-2ubuntu0.1_amd64.deb +nc_pool="http://security.ubuntu.com/ubuntu/pool/universe/n/ncurses" +nc_ver="$(dpkg-query -W -f='${Version}' libtinfo6)" +libtinfo5_deb="libtinfo5_${nc_ver}_amd64.deb" +if ! wget -q "${nc_pool}/${libtinfo5_deb}"; then + libtinfo5_deb="$(wget -qO- "${nc_pool}/" \ + | grep -oE 'libtinfo5_[0-9][^"]*_amd64\.deb' | sort -Vu | tail -1)" + wget -q "${nc_pool}/${libtinfo5_deb}" +fi +sudo apt install -y -qq "./${libtinfo5_deb}" # Start the build process WAMR_DIR=${PWD} diff --git a/.github/scripts/codeql_fail_on_error.py b/.github/scripts/codeql_fail_on_error.py index f150c38a25..1a6bcdba56 100755 --- a/.github/scripts/codeql_fail_on_error.py +++ b/.github/scripts/codeql_fail_on_error.py @@ -61,9 +61,15 @@ def codeql_sarif_contain_error(filename, dismissed_alerts): s = json.load(f) for run in s.get("runs", []): - rules_metadata = run["tool"]["driver"]["rules"] + # Rule metadata isn't always in one place: CodeQL keeps it on the tool + # driver, but rules contributed by a query-pack extension live on that + # extension instead, and the driver's list is empty when nothing fired. + # So read the driver rules, then fall back to gathering them from the + # extensions rather than assuming a fixed location. + rules_metadata = run["tool"]["driver"].get("rules") or [] if not rules_metadata: - rules_metadata = run["tool"]["extensions"][0]["rules"] + for ext in run["tool"].get("extensions", []): + rules_metadata += ext.get("rules", []) for res in run.get("results", []): if "ruleIndex" in res: diff --git a/.github/workflows/build_docker_images.yml b/.github/workflows/build_docker_images.yml index 9c4371b4e7..2b4a6bba3f 100644 --- a/.github/workflows/build_docker_images.yml +++ b/.github/workflows/build_docker_images.yml @@ -26,7 +26,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v7.0.0 - name: Build and save Docker image(wasm-debug-server:${{ inputs.ver_num }}) to tar file run: | diff --git a/.github/workflows/build_iwasm_release.yml b/.github/workflows/build_iwasm_release.yml index ab84c4f43f..314164763a 100644 --- a/.github/workflows/build_iwasm_release.yml +++ b/.github/workflows/build_iwasm_release.yml @@ -104,11 +104,11 @@ jobs: contents: write # for uploading release artifacts steps: - - uses: actions/checkout@v6.0.2 + - uses: actions/checkout@v7.0.0 - name: get cached LLVM libraries id: retrieve_llvm_libs - uses: actions/cache@v5 + uses: actions/cache@v6 with: path: | ./core/deps/llvm/build/bin diff --git a/.github/workflows/build_llvm_libraries.yml b/.github/workflows/build_llvm_libraries.yml index 65c787d668..eedfeef579 100644 --- a/.github/workflows/build_llvm_libraries.yml +++ b/.github/workflows/build_llvm_libraries.yml @@ -45,7 +45,7 @@ jobs: steps: - name: checkout - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v7.0.0 - name: install dependencies for non macos if: ${{ !startsWith(inputs.os, 'macos') }} @@ -79,7 +79,7 @@ jobs: - name: Cache LLVM libraries id: retrieve_llvm_libs - uses: actions/cache@v5 + uses: actions/cache@v6 with: path: | ./core/deps/llvm/build/bin diff --git a/.github/workflows/build_wamr_lldb.yml b/.github/workflows/build_wamr_lldb.yml index 98a15e653b..5387e98d3a 100644 --- a/.github/workflows/build_wamr_lldb.yml +++ b/.github/workflows/build_wamr_lldb.yml @@ -55,7 +55,7 @@ jobs: contents: write # for uploading release artifacts steps: - - uses: actions/checkout@v6.0.2 + - uses: actions/checkout@v7.0.0 - name: download and install wasi-sdk run: | @@ -68,7 +68,7 @@ jobs: - name: Cache build id: lldb_build_cache - uses: actions/cache@v5 + uses: actions/cache@v6 with: path: | ./core/deps/llvm-project/build/bin diff --git a/.github/workflows/build_wamr_sdk.yml b/.github/workflows/build_wamr_sdk.yml index df1f26c748..2b83e4928c 100644 --- a/.github/workflows/build_wamr_sdk.yml +++ b/.github/workflows/build_wamr_sdk.yml @@ -45,7 +45,7 @@ jobs: contents: write # for uploading release artifacts steps: - - uses: actions/checkout@v6.0.2 + - uses: actions/checkout@v7.0.0 - name: download wamr-app-framework run: | diff --git a/.github/workflows/build_wamr_vscode_ext.yml b/.github/workflows/build_wamr_vscode_ext.yml index 7410fd0955..a4838eb277 100644 --- a/.github/workflows/build_wamr_vscode_ext.yml +++ b/.github/workflows/build_wamr_vscode_ext.yml @@ -24,7 +24,7 @@ jobs: contents: write # for uploading release artifacts steps: - - uses: actions/checkout@v6.0.2 + - uses: actions/checkout@v7.0.0 - name: Use Node.js 18.x uses: actions/setup-node@v6 diff --git a/.github/workflows/build_wamr_wasi_extensions.yml b/.github/workflows/build_wamr_wasi_extensions.yml index 21a07a1cc1..163e0d029d 100644 --- a/.github/workflows/build_wamr_wasi_extensions.yml +++ b/.github/workflows/build_wamr_wasi_extensions.yml @@ -28,7 +28,7 @@ jobs: os: [ubuntu-22.04] steps: - name: checkout - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v7.0.0 - name: install-wasi-sdk-wabt uses: ./.github/actions/install-wasi-sdk-wabt diff --git a/.github/workflows/build_wamrc.yml b/.github/workflows/build_wamrc.yml index 9fbe38c0a0..c22e419a80 100644 --- a/.github/workflows/build_wamrc.yml +++ b/.github/workflows/build_wamrc.yml @@ -41,11 +41,11 @@ jobs: contents: write # for uploading release artifacts steps: - - uses: actions/checkout@v6.0.2 + - uses: actions/checkout@v7.0.0 - name: get cached LLVM libraries id: retrieve_llvm_libs - uses: actions/cache@v5 + uses: actions/cache@v6 with: path: | ./core/deps/llvm/build/bin diff --git a/.github/workflows/check_version_h.yml b/.github/workflows/check_version_h.yml index 4ea4c1105e..1a4a172dd3 100644 --- a/.github/workflows/check_version_h.yml +++ b/.github/workflows/check_version_h.yml @@ -14,7 +14,7 @@ jobs: steps: - name: checkout - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v7.0.0 - name: cmake execute to generate version.h run: cmake -B build_version -S . diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index ecc30954b8..77827759e1 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -9,17 +9,35 @@ on: push: branches: - dev/** + # scan pull requests so findings surface on the PR instead of only post-merge + pull_request: # midnight UTC on the latest commit on the main branch schedule: - cron: "0 0 * * *" # allow to be triggered manually workflow_dispatch: +# Serialize CodeQL runs per PR / branch. Only a pull_request cancels its own +# superseded run - once a PR head is replaced the old analysis is throwaway. +# Pushes to dev/** and the nightly cron are left to finish, so branch and +# scheduled scans are never dropped. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: analyze: - # only run this job if the repository is not a fork - # if want to run this job on a fork, please remove the if condition - if: github.repository == 'bytecodealliance/wasm-micro-runtime' + # Pull requests: only scan when the head branch lives in this same + # repository. A pull request from a fork runs with a read-only GITHUB_TOKEN + # (no `security-events: write`), so uploading results and reading + # code-scanning alerts is not permitted and the job would fail; skip it + # cleanly instead. Other events (push to dev/**, the nightly cron, manual + # runs) keep the original behavior of running only on the upstream repo. + if: >- + (github.event_name == 'pull_request' && + github.event.pull_request.head.repo.full_name == github.repository) + || (github.event_name != 'pull_request' && + github.repository == 'bytecodealliance/wasm-micro-runtime') name: Analyze # Runner size impacts CodeQL analysis time. To learn more, please see: # - https://gh.io/recommended-hardware-resources-for-running-codeql @@ -43,13 +61,13 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v7.0.0 with: submodules: recursive # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v4.36.0 + uses: github/codeql-action/init@v4.37.0 with: languages: ${{ matrix.language }} # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs @@ -61,7 +79,7 @@ jobs: ./.github/scripts/codeql_buildscript.sh || exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4.36.0 + uses: github/codeql-action/analyze@v4.37.0 with: category: "/language:${{matrix.language}}" upload: false @@ -114,7 +132,7 @@ jobs: output: ${{ steps.step1.outputs.sarif-output }}/cpp.sarif - name: Upload CodeQL results to code scanning - uses: github/codeql-action/upload-sarif@v4.36.0 + uses: github/codeql-action/upload-sarif@v4.37.0 with: sarif_file: ${{ steps.step1.outputs.sarif-output }} category: "/language:${{matrix.language}}" diff --git a/.github/workflows/coding_guidelines.yml b/.github/workflows/coding_guidelines.yml index 402c455d86..0db0a9b0ef 100644 --- a/.github/workflows/coding_guidelines.yml +++ b/.github/workflows/coding_guidelines.yml @@ -19,13 +19,29 @@ permissions: jobs: compliance_job: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - name: checkout - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v7.0.0 with: fetch-depth: 0 + - name: install clang-format-21 + run: | + # clang-format 21 matches the LLVM shipped in Xcode 26/27, so + # contributors who build or auto-format on macOS/Xcode get the same + # result as CI and other LLVM-built platforms - no formatting + # surprises. It is a small (~50 MB) apt package from apt.llvm.org, + # not a full LLVM toolchain download. + wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key \ + | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc >/dev/null + . /etc/os-release + echo "deb http://apt.llvm.org/${VERSION_CODENAME}/ llvm-toolchain-${VERSION_CODENAME}-21 main" \ + | sudo tee /etc/apt/sources.list.d/llvm.list >/dev/null + sudo apt-get update -qq + sudo apt-get install -y -qq clang-format-21 + clang-format-21 --version + # github.event.pull_request.base.label = ${{github.repository}}/${{github.base_ref}} - name: Run Coding Guidelines Checks run: /usr/bin/env python3 ./ci/coding_guidelines_check.py --commits ${{ github.event.pull_request.base.sha }}..HEAD diff --git a/.github/workflows/compilation_on_android_ubuntu.yml b/.github/workflows/compilation_on_android_ubuntu.yml index e945887b2b..76edbb886c 100644 --- a/.github/workflows/compilation_on_android_ubuntu.yml +++ b/.github/workflows/compilation_on_android_ubuntu.yml @@ -100,7 +100,7 @@ jobs: llvm_cache_key: ${{ needs.build_llvm_libraries_on_ubuntu_2204.outputs.cache_key }} steps: - name: checkout - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6.0.3 # since jobs.id can't contain the dot character # it is hard to use `format` to assemble the cache key @@ -271,7 +271,7 @@ jobs: extra_options: "-DWAMR_BUILD_SIMD=0" steps: - name: checkout - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6.0.3 # only download llvm cache when needed - name: Get LLVM libraries @@ -322,7 +322,7 @@ jobs: steps: - name: checkout - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6.0.3 with: submodules: recursive @@ -381,7 +381,7 @@ jobs: steps: - name: checkout - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6.0.3 - name: Get LLVM libraries id: retrieve_llvm_libs @@ -446,7 +446,7 @@ jobs: steps: - name: checkout - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6.0.3 - name: Get LLVM libraries id: retrieve_llvm_libs @@ -503,7 +503,7 @@ jobs: steps: - name: checkout - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6.0.3 - name: Get LLVM libraries id: retrieve_llvm_libs @@ -602,9 +602,29 @@ jobs: mkdir build && cd build cmake .. cmake --build . --config Debug --parallel 4 - ./iwasm wasm-apps/trap.wasm | grep "#" > call_stack.txt - ./iwasm wasm-apps/trap.aot | grep "#" > call_stack_aot.txt - bash -x ../symbolicate.sh + cd .. + ./verify.sh + + - name: Install binaryen (for debug-tools-optimized) + run: | + sudo mkdir -p /opt/binaryen + curl -L https://github.com/WebAssembly/binaryen/releases/download/version_117/binaryen-version_117-x86_64-linux.tar.gz \ + | sudo tar xz -C /opt/binaryen --strip-components=1 + /opt/binaryen/bin/wasm-opt --version + + - name: Build Sample [debug-tools-optimized] + run: | + cd samples/debug-tools-optimized + for INTERP_FLAG in "" "-DUSE_FAST_INTERP=ON"; do + rm -rf build && mkdir build && cd build + cmake .. $INTERP_FLAG + make -j$(nproc) + cd .. + ./verify.sh oob wasm + ./verify.sh oob aot + ./verify.sh stackoverflow wasm + ./verify.sh stackoverflow aot + done - name: Build Sample [native-stack-overflow] run: | @@ -677,7 +697,7 @@ jobs: steps: - name: checkout - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6.0.3 - name: Set-up OCaml uses: ocaml/setup-ocaml@v3 diff --git a/.github/workflows/compilation_on_macos.yml b/.github/workflows/compilation_on_macos.yml index 30c9b0565b..e3afadb58d 100644 --- a/.github/workflows/compilation_on_macos.yml +++ b/.github/workflows/compilation_on_macos.yml @@ -89,7 +89,7 @@ jobs: llvm_cache_key: ${{ needs.build_llvm_libraries_on_arm_macos.outputs.cache_key }} steps: - name: checkout - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6.0.3 - name: Get LLVM libraries id: retrieve_llvm_libs @@ -191,7 +191,7 @@ jobs: extra_options: "-DWAMR_BUILD_SIMD=0" steps: - name: checkout - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6.0.3 # only download llvm cache when needed - name: Get LLVM libraries @@ -253,7 +253,7 @@ jobs: steps: - name: checkout - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6.0.3 - name: Get LLVM libraries id: retrieve_llvm_libs @@ -311,7 +311,7 @@ jobs: llvm_cache_key: ${{ needs.build_llvm_libraries_on_arm_macos.outputs.cache_key }} steps: - name: checkout - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6.0.3 - name: install-wasi-sdk-wabt uses: ./.github/actions/install-wasi-sdk-wabt @@ -413,9 +413,8 @@ jobs: mkdir build && cd build cmake .. cmake --build . --config Debug --parallel 4 - ./iwasm wasm-apps/trap.wasm | grep "#" > call_stack.txt - ./iwasm wasm-apps/trap.aot | grep "#" > call_stack_aot.txt - bash -x ../symbolicate.sh + cd .. + ./verify.sh - name: Build Sample [native-stack-overflow] run: | diff --git a/.github/workflows/compilation_on_nuttx.yml b/.github/workflows/compilation_on_nuttx.yml index 1ad28f3bb8..63bb28a81e 100644 --- a/.github/workflows/compilation_on_nuttx.yml +++ b/.github/workflows/compilation_on_nuttx.yml @@ -87,21 +87,21 @@ jobs: steps: - name: Checkout NuttX - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v7.0.0 with: repository: apache/nuttx ref: 09a71ec7c16c43398d5acbdcbeee7b08736c3170 path: nuttx - name: Checkout NuttX Apps - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v7.0.0 with: repository: apache/nuttx-apps ref: 6bd593459c4af3cef325c3d22bccd5537a8ed755 path: apps - name: Checkout WAMR - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v7.0.0 with: repository: ${{ github.repository }} path: apps/interpreters/wamr/wamr @@ -126,7 +126,7 @@ jobs: run: make -j$(nproc) EXTRAFLAGS=-Werror - name: Checkout Bloaty - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v7.0.0 with: repository: google/bloaty submodules: recursive diff --git a/.github/workflows/compilation_on_sgx.yml b/.github/workflows/compilation_on_sgx.yml index 6791d31aaf..5f37bc6266 100644 --- a/.github/workflows/compilation_on_sgx.yml +++ b/.github/workflows/compilation_on_sgx.yml @@ -123,7 +123,7 @@ jobs: make_options_feature: "-DWAMR_BUILD_SIMD=0" steps: - name: checkout - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v7.0.0 - name: install SGX SDK and necessary libraries uses: ./.github/actions/install-linux-sgx @@ -166,7 +166,7 @@ jobs: steps: - name: checkout - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v7.0.0 - name: install-wasi-sdk-wabt uses: ./.github/actions/install-wasi-sdk-wabt @@ -190,7 +190,7 @@ jobs: - name: Get LLVM libraries if: matrix.iwasm_make_options_run_mode == '$AOT_BUILD_OPTIONS' id: retrieve_llvm_libs - uses: actions/cache@v5 + uses: actions/cache@v6 with: path: | ./core/deps/llvm/build/bin @@ -262,12 +262,12 @@ jobs: steps: - name: checkout - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v7.0.0 - name: Get LLVM libraries if: matrix.running_mode == 'aot' id: retrieve_llvm_libs - uses: actions/cache@v5 + uses: actions/cache@v6 with: path: | ./core/deps/llvm/build/bin diff --git a/.github/workflows/compilation_on_windows.yml b/.github/workflows/compilation_on_windows.yml index 56adbe7ef6..d1bfbfd3ab 100644 --- a/.github/workflows/compilation_on_windows.yml +++ b/.github/workflows/compilation_on_windows.yml @@ -84,7 +84,7 @@ jobs: "-DWAMR_BUILD_LIBC_UVWASI=0 -DWAMR_BUILD_LIBC_WASI=1", ] steps: - - uses: actions/checkout@v6.0.2 + - uses: actions/checkout@v7.0.0 - name: clone uvwasi library if: ${{ !contains(matrix.build_options, '-DWAMR_BUILD_LIBC_UVWASI=0') }} @@ -108,13 +108,13 @@ jobs: llvm_cache_key: ${{ needs.build_llvm_libraries_on_windows.outputs.cache_key }} steps: - name: checkout - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v7.0.0 # since jobs.id can't contain the dot character # it is hard to use `format` to assemble the cache key - name: Get LLVM libraries id: retrieve_llvm_libs - uses: actions/cache@v5 + uses: actions/cache@v6 with: path: | ./core/deps/llvm/build/bin @@ -150,7 +150,7 @@ jobs: ] steps: - name: checkout - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v7.0.0 - name: download and install wasi-sdk if: matrix.test_option == '$WASI_TEST_OPTIONS' diff --git a/.github/workflows/compilation_on_zephyr.yml b/.github/workflows/compilation_on_zephyr.yml index 26abb90106..a01ef33bd9 100644 --- a/.github/workflows/compilation_on_zephyr.yml +++ b/.github/workflows/compilation_on_zephyr.yml @@ -106,7 +106,7 @@ jobs: # └─── application/ --> DUMMY. keep west_lite.yml here - name: Checkout code - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v7.0.0 with: path: modules/wasm-micro-runtime diff --git a/.github/workflows/create_tag.yml b/.github/workflows/create_tag.yml index 2ca4e8283a..5091616fa1 100644 --- a/.github/workflows/create_tag.yml +++ b/.github/workflows/create_tag.yml @@ -29,7 +29,7 @@ jobs: contents: write # create and push tags steps: - - uses: actions/checkout@v6.0.2 + - uses: actions/checkout@v7.0.0 # Full git history is needed to get a proper list of commits and tags with: fetch-depth: 0 diff --git a/.github/workflows/hadolint_dockerfiles.yml b/.github/workflows/hadolint_dockerfiles.yml index 8f4051f86b..c36ebace41 100644 --- a/.github/workflows/hadolint_dockerfiles.yml +++ b/.github/workflows/hadolint_dockerfiles.yml @@ -37,7 +37,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v7.0.0 # on default, hadolint will fail on warnings and errors - name: Run hadolint on dockerfiles diff --git a/.github/workflows/nightly_run.yml b/.github/workflows/nightly_run.yml index 15b58f7d5d..9ec2a9cb95 100644 --- a/.github/workflows/nightly_run.yml +++ b/.github/workflows/nightly_run.yml @@ -67,7 +67,7 @@ jobs: llvm_cache_key: ${{ needs.build_llvm_libraries_on_ubuntu.outputs.cache_key }} steps: - name: checkout - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6.0.3 # since jobs.id can't contain the dot character # it is hard to use `format` to assemble the cache key @@ -235,7 +235,7 @@ jobs: steps: - name: checkout - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6.0.3 # only download llvm cache when needed - name: Get LLVM libraries @@ -286,7 +286,7 @@ jobs: steps: - name: checkout - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6.0.3 with: submodules: recursive @@ -470,7 +470,7 @@ jobs: steps: - name: checkout - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6.0.3 - name: Get LLVM libraries id: retrieve_llvm_libs @@ -523,7 +523,7 @@ jobs: llvm_cache_key: ${{ needs.build_llvm_libraries_on_ubuntu.outputs.cache_key }} steps: - name: checkout - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6.0.3 - name: install-wasi-sdk-wabt uses: ./.github/actions/install-wasi-sdk-wabt @@ -667,6 +667,43 @@ jobs: ./build.sh --aot ./run.sh --aot + addr2line_tests_multi_sdk: + runs-on: ubuntu-22.04 + steps: + - name: checkout + uses: actions/checkout@v6.0.2 + + - name: Install wasi-sdk 29.0 + run: | + sudo mkdir -p /opt/wasi-sdk-29.0-x86_64-linux + curl -L https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-29/wasi-sdk-29.0-x86_64-linux.tar.gz \ + | sudo tar xz -C /opt/wasi-sdk-29.0-x86_64-linux --strip-components=1 + + - name: Install wasi-sdk 33.0 + run: | + sudo mkdir -p /opt/wasi-sdk-33.0-x86_64-linux + curl -L https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-33/wasi-sdk-33.0-x86_64-linux.tar.gz \ + | sudo tar xz -C /opt/wasi-sdk-33.0-x86_64-linux --strip-components=1 + + - name: Install wabt + run: | + sudo mkdir -p /opt/wabt + curl -L https://github.com/WebAssembly/wabt/releases/download/1.0.34/wabt-1.0.34-ubuntu.tar.gz \ + | sudo tar xz -C /opt/wabt --strip-components=1 + + - name: Install binaryen + run: | + sudo mkdir -p /opt/binaryen + curl -L https://github.com/WebAssembly/binaryen/releases/download/version_117/binaryen-version_117-x86_64-linux.tar.gz \ + | sudo tar xz -C /opt/binaryen --strip-components=1 + + - name: Install pytest + run: pip install pytest + + - name: Run addr2line.py tests across both SDKs + working-directory: test-tools/addr2line/tests + run: ./run_tests.sh --multi-sdk -v + test: needs: [build_iwasm, build_llvm_libraries_on_ubuntu, build_wamrc] runs-on: ${{ matrix.os }} @@ -725,7 +762,7 @@ jobs: sanitizer: ubsan steps: - name: checkout - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6.0.3 - name: install-wasi-sdk-wabt if: matrix.test_option == '$WASI_TEST_OPTIONS' diff --git a/.github/workflows/release_process.yml b/.github/workflows/release_process.yml index b7eebd1c52..a30bb83a7b 100644 --- a/.github/workflows/release_process.yml +++ b/.github/workflows/release_process.yml @@ -55,7 +55,7 @@ jobs: outputs: upload_url: ${{ steps.create_release.outputs.upload_url }} steps: - - uses: actions/checkout@v6.0.2 + - uses: actions/checkout@v7.0.0 - name: prepare the release note run: | diff --git a/.github/workflows/reuse_latest_release_binaries.yml b/.github/workflows/reuse_latest_release_binaries.yml index 3bbd4ac9bf..32cc73bffb 100644 --- a/.github/workflows/reuse_latest_release_binaries.yml +++ b/.github/workflows/reuse_latest_release_binaries.yml @@ -34,7 +34,7 @@ jobs: contents: write # for creating realease and uploading release artifacts steps: - - uses: actions/checkout@v6.0.2 + - uses: actions/checkout@v7.0.0 # Full git history is needed to get a proper list of commits and tags with: fetch-depth: 0 diff --git a/.github/workflows/spec_test_on_nuttx.yml b/.github/workflows/spec_test_on_nuttx.yml index baf4fb3e11..25d3d9c5c0 100644 --- a/.github/workflows/spec_test_on_nuttx.yml +++ b/.github/workflows/spec_test_on_nuttx.yml @@ -143,21 +143,21 @@ jobs: # Note: we use an unreleased version nuttx for xtensa because # 12.4 doesn't contain necessary esp32s3 changes. - name: Checkout NuttX - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v7.0.0 with: repository: apache/nuttx ref: ${{ matrix.target_config.target == 'xtensa' && '985d395b025cf2012b22f6bb4461959fa6d87645' || '09a71ec7c16c43398d5acbdcbeee7b08736c3170' }} path: nuttx - name: Checkout NuttX Apps - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v7.0.0 with: repository: apache/nuttx-apps ref: ${{ matrix.target_config.target == 'xtensa' && '2ef3eb25c0cec944b13792185f7e5d5a05990d5f' || '6bd593459c4af3cef325c3d22bccd5537a8ed755' }} path: apps - name: Checkout WAMR - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v7.0.0 with: repository: ${{ github.repository }} path: apps/interpreters/wamr/wamr @@ -165,7 +165,7 @@ jobs: - name: Get LLVM libraries if: contains(matrix.wamr_test_option.mode, 'aot') id: retrieve_llvm_libs - uses: actions/cache@v5 + uses: actions/cache@v6 with: path: | ./core/deps/llvm/build/bin diff --git a/.github/workflows/supply_chain.yml b/.github/workflows/supply_chain.yml index 5efdc62c3d..73886b5f12 100644 --- a/.github/workflows/supply_chain.yml +++ b/.github/workflows/supply_chain.yml @@ -34,7 +34,7 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v3.1.0 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v3.1.0 with: persist-credentials: false @@ -60,6 +60,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@0e150e40762c1253b364a04b0fc9f2cc14effff2 + uses: github/codeql-action/upload-sarif@8096e4f9571939c23cbd19018c44c665f90096af with: sarif_file: results.sarif diff --git a/.github/workflows/wamr_wasi_extensions.yml b/.github/workflows/wamr_wasi_extensions.yml index 0ef0b5b123..b16bf0399c 100644 --- a/.github/workflows/wamr_wasi_extensions.yml +++ b/.github/workflows/wamr_wasi_extensions.yml @@ -30,7 +30,7 @@ jobs: os: [ubuntu-22.04, macos-15-intel, macos-15] steps: - name: checkout - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v7.0.0 - name: install-wasi-sdk-wabt uses: ./.github/actions/install-wasi-sdk-wabt diff --git a/build-scripts/unsupported_combination.cmake b/build-scripts/unsupported_combination.cmake index 4284be32bf..1789c71b52 100644 --- a/build-scripts/unsupported_combination.cmake +++ b/build-scripts/unsupported_combination.cmake @@ -64,7 +64,17 @@ endfunction() if(WAMR_BUILD_EXCE_HANDLING EQUAL 1) check_aot_mode_error("Unsupported build configuration: EXCE_HANDLING + AOT") - check_fast_interp_error("Unsupported build configuration: EXCE_HANDLING + FAST_INTERP") + # FAST_INTERP + EXCE_HANDLING is supported for *throw-only* shapes: + # WASM modules that declare tags and execute throw / rethrow without + # ever entering a same-function try / catch handler. The throw + # propagates to the caller via the existing got_exception bailout + # path, exactly like any other trap. This covers Porffor (its + # JS-to-wasm compiler emits 0 try/catch handlers; every JS throw + # escapes to the host). Modules that contain WASM_OP_TRY / CATCH / + # CATCH_ALL / DELEGATE still load, but those handlers report + # "unsupported opcode" at runtime — see the WASM_OP_TRY handler in + # core/iwasm/interpreter/wasm_interp_fast.c. Full same-function + # try / catch lowering is the natural follow-up. check_fast_jit_error("Unsupported build configuration: EXCE_HANDLING + FAST_JIT") check_llvm_jit_error("Unsupported build configuration: EXCE_HANDLING + JIT") endif() diff --git a/ci/coding_guidelines_check.py b/ci/coding_guidelines_check.py index 1901a48a97..1b92e15c69 100644 --- a/ci/coding_guidelines_check.py +++ b/ci/coding_guidelines_check.py @@ -12,8 +12,8 @@ import sys import unittest -CLANG_FORMAT_CMD = "clang-format-14" -GIT_CLANG_FORMAT_CMD = "git-clang-format-14" +CLANG_FORMAT_CMD = "clang-format-21" +GIT_CLANG_FORMAT_CMD = "git-clang-format-21" # glob style patterns EXCLUDE_PATHS = [ @@ -92,29 +92,29 @@ def run_clang_format(file_path: Path, root: Path) -> bool: def run_clang_format_diff(root: Path, commits: str) -> bool: """ - Use `clang-format-14` or `git-clang-format-14` to check code format of + Use `clang-format-21` or `git-clang-format-21` to check code format of the PR, with a commit range specified. It is required to format the code before committing the PR, or it might fail to pass the CI check: - 1. Install clang-format-14.0.0 + 1. Install clang-format-21 You can download the package from https://github.com/llvm/llvm-project/releases and install it. For Debian/Ubuntu, we can probably use - `sudo apt-get install clang-format-14`. + `sudo apt-get install clang-format-21`. - Homebrew has it as a part of llvm@14. + Homebrew has it as a part of llvm@21. ```shell - brew install llvm@14 - /usr/local/opt/llvm@14/bin/clang-format + brew install llvm@21 + /usr/local/opt/llvm@21/bin/clang-format ``` 2. Format the C/C++ source file ``` shell cd path/to/wamr/root - clang-format-14 --style file -i path/to/file + clang-format-21 --style file -i path/to/file ``` The code wrapped by `/* clang-format off */` and `/* clang-format on */` diff --git a/ci/pre_commit_hook_sample b/ci/pre_commit_hook_sample index 682e789464..2d1878d998 100755 --- a/ci/pre_commit_hook_sample +++ b/ci/pre_commit_hook_sample @@ -4,7 +4,7 @@ # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception # This is a sample of pre-commit hook that can be used to make your code fit the WAMR CI code style requirements. -# You need to have clang-format-12 installed to use this hook. +# You need to have clang-format-21 installed to use this hook. # To add this pre-commit hook, copy it to /.git/hooks/pre-commit # (you don't need any extensions here) @@ -22,10 +22,10 @@ is_c_or_cpp_file() { # Loop through staged files and apply command "abc" to C and C++ files for staged_file in $(git diff --cached --name-only); do if is_c_or_cpp_file "$staged_file"; then - clang-format-12 -Werror --style file --dry-run "$staged_file" 2>/dev/null + clang-format-21 -Werror --style file --dry-run "$staged_file" 2>/dev/null if [ $? -ne 0 ]; then echo "Issues are found in $staged_file. Applying the fix" - clang-format-12 --style file -i "$staged_file" + clang-format-21 --style file -i "$staged_file" fi git add "$staged_file" # Add the modified file back to staging fi diff --git a/core/iwasm/aot/aot_runtime.c b/core/iwasm/aot/aot_runtime.c index d1ec873c58..b7ca89b42d 100644 --- a/core/iwasm/aot/aot_runtime.c +++ b/core/iwasm/aot/aot_runtime.c @@ -5553,7 +5553,8 @@ aot_const_str_set_insert(const uint8 *str, int32 len, AOTModule *module, #if WASM_ENABLE_DYNAMIC_AOT_DEBUG != 0 AOTModule *g_dynamic_aot_module = NULL; -void __attribute__((noinline)) __enable_dynamic_aot_debug(void) +void __attribute__((noinline)) +__enable_dynamic_aot_debug(void) { /* empty implementation. */ } diff --git a/core/iwasm/aot/arch/aot_reloc_riscv.c b/core/iwasm/aot/arch/aot_reloc_riscv.c index 8df9f9f8ed..d05ed0d764 100644 --- a/core/iwasm/aot/arch/aot_reloc_riscv.c +++ b/core/iwasm/aot/arch/aot_reloc_riscv.c @@ -325,10 +325,7 @@ typedef struct RelocTypeStrMap { char *reloc_str; } RelocTypeStrMap; -#define RELOC_TYPE_MAP(reloc_type) \ - { \ - reloc_type, #reloc_type \ - } +#define RELOC_TYPE_MAP(reloc_type) { reloc_type, #reloc_type } static RelocTypeStrMap reloc_type_str_maps[] = { RELOC_TYPE_MAP(R_RISCV_32), RELOC_TYPE_MAP(R_RISCV_64), diff --git a/core/iwasm/aot/arch/aot_reloc_x86_32.c b/core/iwasm/aot/arch/aot_reloc_x86_32.c index f7f2421f48..9776f3fdb6 100644 --- a/core/iwasm/aot/arch/aot_reloc_x86_32.c +++ b/core/iwasm/aot/arch/aot_reloc_x86_32.c @@ -30,35 +30,42 @@ void __umoddi3(); #pragma function(floor) #pragma function(ceil) -static int64 __stdcall __divdi3(int64 a, int64 b) +static int64 __stdcall +__divdi3(int64 a, int64 b) { return a / b; } -static uint64 __stdcall __udivdi3(uint64 a, uint64 b) +static uint64 __stdcall +__udivdi3(uint64 a, uint64 b) { return a / b; } -static int64 __stdcall __moddi3(int64 a, int64 b) +static int64 __stdcall +__moddi3(int64 a, int64 b) { return a % b; } -static uint64 __stdcall __umoddi3(uint64 a, uint64 b) +static uint64 __stdcall +__umoddi3(uint64 a, uint64 b) { return a % b; } -static uint64 __stdcall __aulldiv(uint64 a, uint64 b) +static uint64 __stdcall +__aulldiv(uint64 a, uint64 b) { return a / b; } -static int64 __stdcall __alldiv(int64 a, int64 b) +static int64 __stdcall +__alldiv(int64 a, int64 b) { return a / b; } -static int64 __stdcall __allrem(int64 a, int64 b) +static int64 __stdcall +__allrem(int64 a, int64 b) { return a % b; } diff --git a/core/iwasm/aot/arch/aot_reloc_x86_64.c b/core/iwasm/aot/arch/aot_reloc_x86_64.c index 2f36c4c3bc..d2c2080373 100644 --- a/core/iwasm/aot/arch/aot_reloc_x86_64.c +++ b/core/iwasm/aot/arch/aot_reloc_x86_64.c @@ -157,7 +157,7 @@ apply_relocation(AOTModule *module, uint8 *target_section_addr, case R_X86_64_GOTPCRELX: case R_X86_64_REX_GOTPCRELX: { - intptr_t target_addr = (intptr_t) /* S + A - P */ + intptr_t target_addr = (intptr_t)/* S + A - P */ ((uintptr_t)symbol_addr + reloc_addend - (uintptr_t)(target_section_addr + reloc_offset)); @@ -176,7 +176,7 @@ apply_relocation(AOTModule *module, uint8 *target_section_addr, } case R_X86_64_PC64: { - intptr_t target_addr = (intptr_t) /* S + A - P */ + intptr_t target_addr = (intptr_t)/* S + A - P */ ((uintptr_t)symbol_addr + reloc_addend - (uintptr_t)(target_section_addr + reloc_offset)); @@ -226,12 +226,12 @@ apply_relocation(AOTModule *module, uint8 *target_section_addr, plt = (uint8 *)module->code + module->code_size - get_plt_table_size() + get_plt_item_size() * symbol_index; - target_addr = (intptr_t) /* L + A - P */ + target_addr = (intptr_t)/* L + A - P */ ((uintptr_t)plt + reloc_addend - (uintptr_t)(target_section_addr + reloc_offset)); } else { - target_addr = (intptr_t) /* S + A - P */ + target_addr = (intptr_t)/* S + A - P */ ((uintptr_t)symbol_addr + reloc_addend - (uintptr_t)(target_section_addr + reloc_offset)); } diff --git a/core/iwasm/aot/debug/elf.h b/core/iwasm/aot/debug/elf.h index 9bdad6521d..154f72193e 100644 --- a/core/iwasm/aot/debug/elf.h +++ b/core/iwasm/aot/debug/elf.h @@ -104,10 +104,7 @@ /* EI_NIDENT is defined in "Included Files" section */ #define EI_MAGIC_SIZE 4 -#define EI_MAGIC \ - { \ - 0x7f, 'E', 'L', 'F' \ - } +#define EI_MAGIC { 0x7f, 'E', 'L', 'F' } #define ELFMAG0 0x7f /* EI_MAG */ #define ELFMAG1 'E' diff --git a/core/iwasm/aot/debug/elf32.h b/core/iwasm/aot/debug/elf32.h index b4b27948ea..6230ef82bd 100644 --- a/core/iwasm/aot/debug/elf32.h +++ b/core/iwasm/aot/debug/elf32.h @@ -32,14 +32,14 @@ ****************************************************************************/ #define ELF32_ST_BIND(i) ((i) >> 4) -#define ELF32_ST_TYPE(i) ((i)&0xf) -#define ELF32_ST_INFO(b, t) (((b) << 4) | ((t)&0xf)) +#define ELF32_ST_TYPE(i) ((i) & 0xf) +#define ELF32_ST_INFO(b, t) (((b) << 4) | ((t) & 0xf)) /* Definitions for Elf32_Rel*::r_info */ #define ELF32_R_SYM(i) ((i) >> 8) -#define ELF32_R_TYPE(i) ((i)&0xff) -#define ELF32_R_INFO(s, t) (((s) << 8) | ((t)&0xff)) +#define ELF32_R_TYPE(i) ((i) & 0xff) +#define ELF32_R_INFO(s, t) (((s) << 8) | ((t) & 0xff)) #if 0 #define ELF_R_SYM(i) ELF32_R_SYM(i) diff --git a/core/iwasm/aot/debug/elf64.h b/core/iwasm/aot/debug/elf64.h index 499c737c11..7136c82ed6 100644 --- a/core/iwasm/aot/debug/elf64.h +++ b/core/iwasm/aot/debug/elf64.h @@ -36,8 +36,8 @@ /* Definitions for Elf64_Rel*::r_info */ #define ELF64_R_SYM(i) ((i) >> 32) -#define ELF64_R_TYPE(i) ((i)&0xffffffffL) -#define ELF64_R_INFO(s, t) (((s) << 32) + ((t)&0xffffffffL)) +#define ELF64_R_TYPE(i) ((i) & 0xffffffffL) +#define ELF64_R_INFO(s, t) (((s) << 32) + ((t) & 0xffffffffL)) #if 0 #define ELF_R_SYM(i) ELF64_R_SYM(i) diff --git a/core/iwasm/common/gc/stringref/stringref_stub.c b/core/iwasm/common/gc/stringref/stringref_stub.c index 8a6d624967..7bceae91b2 100644 --- a/core/iwasm/common/gc/stringref/stringref_stub.c +++ b/core/iwasm/common/gc/stringref/stringref_stub.c @@ -11,7 +11,8 @@ /******************* gc finalizer *****************/ void wasm_string_destroy(WASMString str_obj) -{} +{ +} /******************* opcode functions *****************/ @@ -133,4 +134,5 @@ wasm_string_rewind(WASMString str_obj, uint32 pos, uint32 count, void wasm_string_dump(WASMString str_obj) -{} +{ +} diff --git a/core/iwasm/common/wasm_blocking_op.c b/core/iwasm/common/wasm_blocking_op.c index 25777c8d74..b6d6bb972f 100644 --- a/core/iwasm/common/wasm_blocking_op.c +++ b/core/iwasm/common/wasm_blocking_op.c @@ -87,6 +87,7 @@ wasm_runtime_begin_blocking_op(wasm_exec_env_t env) void wasm_runtime_end_blocking_op(wasm_exec_env_t env) -{} +{ +} #endif /* WASM_ENABLE_THREAD_MGR && OS_ENABLE_WAKEUP_BLOCKING_OP */ diff --git a/core/iwasm/common/wasm_c_api.c b/core/iwasm/common/wasm_c_api.c index 53d3a87f04..8b6d4ba5e4 100644 --- a/core/iwasm/common/wasm_c_api.c +++ b/core/iwasm/common/wasm_c_api.c @@ -3248,11 +3248,12 @@ wasm_func_copy(const wasm_func_t *func) return NULL; } - if (!(cloned = func->with_env ? wasm_func_new_with_env_basic( - func->store, func->type, func->u.cb_env.cb, - func->u.cb_env.env, func->u.cb_env.finalizer) - : wasm_func_new_basic(func->store, func->type, - func->u.cb))) { + if (!(cloned = + func->with_env + ? wasm_func_new_with_env_basic( + func->store, func->type, func->u.cb_env.cb, + func->u.cb_env.env, func->u.cb_env.finalizer) + : wasm_func_new_basic(func->store, func->type, func->u.cb))) { goto failed; } diff --git a/core/iwasm/compilation/aot_emit_aot_file.c b/core/iwasm/compilation/aot_emit_aot_file.c index 12749305b7..3628df02e0 100644 --- a/core/iwasm/compilation/aot_emit_aot_file.c +++ b/core/iwasm/compilation/aot_emit_aot_file.c @@ -19,9 +19,9 @@ #define CHECK_SIZE(size) \ do { \ - if (size == (uint32)-1) { \ + if (size == (uint32) - 1) { \ aot_set_last_error("get symbol size failed."); \ - return (uint32)-1; \ + return (uint32) - 1; \ } \ } while (0) @@ -3674,7 +3674,7 @@ aot_resolve_stack_sizes(AOTCompContext *comp_ctx, AOTObjectData *obj_data) unsigned int stack_consumption_to_call_wrapped_func = musttail ? 0 : aot_estimate_stack_usage_for_function_call( - comp_ctx, func_ctx->aot_func->func_type); + comp_ctx, func_ctx->aot_func->func_type); /* * LLVM seems to eliminate calls to an empty function diff --git a/core/iwasm/compilation/aot_llvm.c b/core/iwasm/compilation/aot_llvm.c index e9f86817f0..52b8aae1a0 100644 --- a/core/iwasm/compilation/aot_llvm.c +++ b/core/iwasm/compilation/aot_llvm.c @@ -2520,7 +2520,7 @@ jit_stack_size_callback(void *user_data, const char *name, size_t namelen, stack_consumption_to_call_wrapped_func = musttail ? 0 : aot_estimate_stack_usage_for_function_call( - comp_ctx, func_ctx->aot_func->func_type); + comp_ctx, func_ctx->aot_func->func_type); LOG_VERBOSE("func %.*s stack %u + %zu + %u", (int)namelen, name, stack_consumption_to_call_wrapped_func, stack_size, call_size); diff --git a/core/iwasm/compilation/aot_orc_extra2.cpp b/core/iwasm/compilation/aot_orc_extra2.cpp index 2979737e76..f97cbac4e9 100644 --- a/core/iwasm/compilation/aot_orc_extra2.cpp +++ b/core/iwasm/compilation/aot_orc_extra2.cpp @@ -41,7 +41,8 @@ MyCompiler::MyCompiler(llvm::orc::JITTargetMachineBuilder JTMB, cb_t cb, , JTMB(std::move(JTMB)) , cb(cb) , cb_data(cb_data) -{} +{ +} class PrintStackSizes : public llvm::MachineFunctionPass { @@ -59,7 +60,8 @@ PrintStackSizes::PrintStackSizes(cb_t cb, void *cb_data) : MachineFunctionPass(ID) , cb(cb) , cb_data(cb_data) -{} +{ +} char PrintStackSizes::ID = 0; diff --git a/core/iwasm/compilation/debug/dwarf_extractor.h b/core/iwasm/compilation/debug/dwarf_extractor.h index 0bacb97fa2..752082a3f8 100644 --- a/core/iwasm/compilation/debug/dwarf_extractor.h +++ b/core/iwasm/compilation/debug/dwarf_extractor.h @@ -14,7 +14,7 @@ extern "C" { typedef unsigned int LLDBLangType; #define LLDB_TO_LLVM_LANG_TYPE(lldb_lang_type) \ - (LLVMDWARFSourceLanguage)(((lldb_lang_type) > 0 ? (lldb_lang_type)-1 : 1)) + (LLVMDWARFSourceLanguage)(((lldb_lang_type) > 0 ? (lldb_lang_type) - 1 : 1)) struct AOTCompData; typedef struct AOTCompData *aot_comp_data_t; diff --git a/core/iwasm/fast-jit/cg/x86-64/jit_codegen_x86_64.cpp b/core/iwasm/fast-jit/cg/x86-64/jit_codegen_x86_64.cpp index 967bf14b5e..8f5fe4045c 100644 --- a/core/iwasm/fast-jit/cg/x86-64/jit_codegen_x86_64.cpp +++ b/core/iwasm/fast-jit/cg/x86-64/jit_codegen_x86_64.cpp @@ -298,7 +298,8 @@ class JitErrorHandler : public ErrorHandler JitErrorHandler() : err(kErrorOk) - {} + { + } void handleError(Error e, const char *msg, BaseEmitter *base) override { diff --git a/core/iwasm/fast-jit/jit_ir.h b/core/iwasm/fast-jit/jit_ir.h index 6b3acfa0b4..6d6b65bb1b 100644 --- a/core/iwasm/fast-jit/jit_ir.h +++ b/core/iwasm/fast-jit/jit_ir.h @@ -1866,14 +1866,14 @@ jit_cc_update_cfg(JitCompContext *cc); */ #define JIT_FOREACH_BLOCK_REVERSE(CC, I, B) \ for ((I) = (CC)->_ann._label_num; (I) > 2; (I)--) \ - if (((B) = (CC)->_ann._label_basic_block[(I)-1])) + if (((B) = (CC)->_ann._label_basic_block[(I) - 1])) /** * The version that includes entry and exit block. */ #define JIT_FOREACH_BLOCK_REVERSE_ENTRY_EXIT(CC, I, B) \ for ((I) = (CC)->_ann._label_num; (I) > 0; (I)--) \ - if (((B) = (CC)->_ann._label_basic_block[(I)-1])) + if (((B) = (CC)->_ann._label_basic_block[(I) - 1])) #ifdef __cplusplus } diff --git a/core/iwasm/interpreter/wasm.h b/core/iwasm/interpreter/wasm.h index c60349d10f..879bdc64b1 100644 --- a/core/iwasm/interpreter/wasm.h +++ b/core/iwasm/interpreter/wasm.h @@ -681,6 +681,54 @@ typedef struct WASMImport { } u; } WASMImport; +#if WASM_ENABLE_EXCE_HANDLING != 0 && WASM_ENABLE_FAST_INTERP != 0 +/* One typed `catch N` clause inside a single try-region. The handler_pc + * points at the first opcode of the catch body in the rewritten fast- + * interp IR; the loader patches it in pass 2 of the preprocess pass. */ +typedef struct WASMFastEHCatch { + uint32 tag_index; + uint8 *handler_pc; + /* Tag-with-params payload routing (same-function dispatch only). + * When this catch matches, the throw walker copies `param_cell_num` + * 32-bit cells from the throw site's *source* slots (encoded as + * `int16` immediates after the THROW opcode in the rewritten IR) + * into these *destination* slots in the catch body's `frame_lp`, + * then sets `frame_ip = handler_pc`. The destination slots are + * allocated by the CATCH loader at preprocess time, mirroring how + * block-with-params allocate fresh `dynamic_offset` slots via + * `PUSH_OFFSET_TYPE`. NULL iff `param_cell_num == 0` (the typical + * tag-without-params shape, e.g. Porffor's empty-payload tags). + * + * Cross-function dispatch (caller's catch fires for a callee's + * throw) does NOT copy the payload: the callee's source slots + * sit in a frame that's about to be torn down by return_func. + * That gap is documented as an ignored integration test — + * `cross_function_tag_with_params` in + * crates/benchmark-core/tests/eh_correctness.rs. */ + uint32 param_cell_num; + int16 *param_dst_offsets; +} WASMFastEHCatch; + +/* One entry per same-function try-region, indexed by the uint32 immediate + * emitted after the rewritten TRY opcode. Allocated once per function at + * load time, sized by `func->exception_handler_count`. At runtime the + * dispatch loop carries one stack-allocated handle per *active* try- + * region (see frame->eh_stack); hot ops (CALL / LOAD / STORE) never + * touch this table. */ +typedef struct WASMFastEHEntry { + uint32 catch_count; + WASMFastEHCatch *catches; /* may be NULL when catch_count == 0 */ + uint8 *catch_all_pc; /* NULL if no `catch_all` clause */ + /* UINT32_MAX iff the try-region closes with `end`; otherwise the + * LEB depth from `delegate N`. */ + uint32 delegate_target_depth; + /* Rewritten-IR pc of the op immediately after the try-region's `end` + * (or `delegate`). CATCH / CATCH_ALL handlers branch here when their + * body completes; the loader patches it when the `end` is seen. */ + uint8 *end_of_region_pc; +} WASMFastEHEntry; +#endif /* WASM_ENABLE_EXCE_HANDLING && WASM_ENABLE_FAST_INTERP */ + struct WASMFunction { #if WASM_ENABLE_CUSTOM_NAME_SECTION != 0 char *field_name; @@ -721,7 +769,19 @@ struct WASMFunction { #endif #if WASM_ENABLE_EXCE_HANDLING != 0 + /* Number of `try` opcodes in this function. Populated by the loader + * during the preprocess pass (classic-interp uses this to size the + * runtime handler-pointer array stored on the value stack; fast- + * interp uses it to size `exception_handlers[]` below). */ uint32 exception_handler_count; +#if WASM_ENABLE_FAST_INTERP != 0 + /* Per-function table of try-regions in source order, length + * `exception_handler_count`. Allocated and populated in pass 2 of + * the fast-interp preprocess pass; the uint32 immediate emitted + * after the rewritten TRY opcode is the index into this array. + * NULL iff `exception_handler_count == 0`. */ + WASMFastEHEntry *exception_handlers; +#endif #endif #if WASM_ENABLE_FAST_JIT != 0 || WASM_ENABLE_JIT != 0 \ diff --git a/core/iwasm/interpreter/wasm_interp.h b/core/iwasm/interpreter/wasm_interp.h index 1416405460..8ca6fe5f23 100644 --- a/core/iwasm/interpreter/wasm_interp.h +++ b/core/iwasm/interpreter/wasm_interp.h @@ -40,6 +40,17 @@ typedef struct WASMInterpFrame { */ bool exception_raised; uint32 tag_index; +#if WASM_ENABLE_FAST_INTERP != 0 + /* Number of *currently-active* try-regions on this frame's eh- + * stack. The stack itself lives in the trailing cells of the + * frame's operand[] block — see call_func_from_entry in + * wasm_interp_fast.c where all_cell_num is grown by + * `exception_handler_count` cells per frame. Read+written only by + * the WASM_OP_TRY / CATCH / CATCH_ALL / END / THROW handlers; the + * hot ops (CALL / LOAD / STORE) never touch it, so this field + * stays cold and clusters with exception_raised/tag_index above. */ + uint32 eh_count; +#endif #endif #if WASM_ENABLE_FAST_INTERP != 0 diff --git a/core/iwasm/interpreter/wasm_interp_classic.c b/core/iwasm/interpreter/wasm_interp_classic.c index abde189ebc..5d23142044 100644 --- a/core/iwasm/interpreter/wasm_interp_classic.c +++ b/core/iwasm/interpreter/wasm_interp_classic.c @@ -2438,7 +2438,7 @@ wasm_interp_call_func_bytecode(WASMModuleInstance *module, else cur_func_type = cur_func->u.func->func_type; - /* clang-format off */ + /* clang-format off */ #if WASM_ENABLE_GC == 0 if (cur_type != cur_func_type) { wasm_set_exception(module, "indirect call type mismatch"); diff --git a/core/iwasm/interpreter/wasm_interp_fast.c b/core/iwasm/interpreter/wasm_interp_fast.c index 937a7fdecf..bda039eb1e 100644 --- a/core/iwasm/interpreter/wasm_interp_fast.c +++ b/core/iwasm/interpreter/wasm_interp_fast.c @@ -25,6 +25,15 @@ #include "simde/wasm/simd128.h" #endif +/* MSVC has no `__builtin_expect`; the cold-path hints below are + * GCC/Clang only. Provide a no-op fallback so the loop still + * compiles on the Windows MSVC build. Branch-predictor hints are + * an optimization, not correctness, so dropping them on MSVC is + * fine. */ +#if !defined(__GNUC__) && !defined(__clang__) +#define __builtin_expect(expr, expected) (expr) +#endif + typedef int32 CellType_I32; typedef int64 CellType_I64; typedef float32 CellType_F32; @@ -102,6 +111,55 @@ typedef float64 CellType_F64; #define CHECK_INSTRUCTION_LIMIT() (void)0 #endif +#if WASM_ENABLE_EXCE_HANDLING != 0 +/* Per-frame eh-stack entries are 2 cells wide. Cell 0 packs the index + * into `func->exception_handlers[]` (low 31 bits) and a state bit + * (top bit): clear when the try-region's handler is *in scope* (TRY + * state — a throw matching one of its catches will dispatch into the + * handler), set once the throw walker has selected one of its + * handlers (CATCH state — further throws raised from inside that + * handler skip the entry and propagate outward). Cell 1 holds the + * wasm tag index of the exception currently being handled (written + * by the throw walker on dispatch; read by RETHROW). The tag is + * undefined while the entry is in TRY state. */ +#define EH_TRY_CATCH_STATE_BIT 0x80000000u +#define EH_ENTRY_CELLS 2 + +/* Base of the per-frame eh-stack, in cells from frame_lp. + * + * Frame setup (see the call-into-wasm-function path) reserves the + * eh-stack region *after* the locals + value stack and, in GC builds, + * *after* the frame_ref root bitmap as well. The accumulation order in + * `all_cell_num` is: + * + * locals + value stack : cell_num_of_local_stack cells + * [GC only] frame_ref : (cell_num_of_local_stack + 3) / 4 cells + * eh-stack : exception_handler_count * EH_ENTRY_CELLS + * + * where cell_num_of_local_stack = param_cell_num + local_cell_num + * + max_stack_cell_num. The runtime pointer must skip the same regions. + * In non-GC builds the frame_ref bitmap doesn't exist, so the offset + * collapses to cell_num_of_local_stack — leaving non-GC behavior byte- + * for-byte identical. In GC builds, omitting the bitmap term made the + * eh-stack alias frame->frame_ref (both start at + * frame_lp + cell_num_of_local_stack), so WASM_OP_TRY corrupted GC + * roots; adding it lands the eh-stack in its reserved trailing cells. */ +#if WASM_ENABLE_GC != 0 +#define EH_FRAME_REF_CELLS(cur_func, cur_wasm_func) \ + ((((cur_func)->param_cell_num + (cur_func)->local_cell_num \ + + (cur_wasm_func)->max_stack_cell_num) \ + + 3) \ + / 4) +#else +#define EH_FRAME_REF_CELLS(cur_func, cur_wasm_func) 0 +#endif + +#define EH_STACK_BASE(frame_lp, cur_func, cur_wasm_func) \ + ((frame_lp) + (cur_func)->param_cell_num + (cur_func)->local_cell_num \ + + (cur_wasm_func)->max_stack_cell_num \ + + EH_FRAME_REF_CELLS(cur_func, cur_wasm_func)) +#endif + static inline uint32 rotl32(uint32 n, uint32 c) { @@ -1538,6 +1596,30 @@ wasm_interp_call_func_bytecode(WASMModuleInstance *module, uint8 *maddr = NULL; uint32 local_idx, local_offset, global_idx; uint8 opcode = 0, local_type, *global_addr; +#if WASM_ENABLE_EXCE_HANDLING != 0 + /* Carries the wasm tag index from WASM_OP_THROW to the + * find_a_catch_handler label, and from a callee's return through + * frame->tag_index back to a caller-side find_a_catch_handler. + * Cold path only — the dispatch loop's hot ops never reference + * this variable, so the compiler is free to spill it. */ + uint32 exception_tag_index = 0; + /* Tag-with-params payload routing for same-function dispatch. + * Read off the IR after THROW's tag_index immediate; + * `throw_src_offsets` points at the first src-slot int16 in the + * rewritten IR, and `throw_param_cell_num` is the total cell + * count across all of the tag's params. find_a_catch_handler + * uses these to copy frame_lp[src[i]] into the matched catch's + * pre-allocated dst slots. Both are cold-path-only — like + * exception_tag_index, the dispatch loop's hot ops never + * reference them. RETHROW re-points throw_src_offsets at the + * still-alive catch's `param_dst_offsets` (the original + * payload values, unchanged by the catch body since they live + * in a different slot range from locals) so the re-raised + * exception carries the same payload across outer try-regions + * in this frame. */ + uint32 throw_param_cell_num = 0; + int16 *throw_src_offsets = NULL; +#endif #if WASM_ENABLE_INSTRUCTION_METERING != 0 int instructions_left = -1; @@ -1774,7 +1856,27 @@ wasm_interp_call_func_bytecode(WASMModuleInstance *module, val = GET_OPERAND(uint32, I32, 0); frame_ip += 2; - if ((uint32)val >= tbl_inst->cur_size) { + /* Bounds / null / type-mismatch checks below are + * structurally cold paths — well-formed wasm modules + * pass them on every dispatched CALL_INDIRECT. Marking + * them `__builtin_expect(cond, 0)` lets the compiler + * (a) hint the branch predictor with a static-bias + * fallback for unseen call sites, and (b) lay out the + * error-handling tail away from the hot path so each + * fall-through case stays in one straight-line I-cache + * line. Apple Silicon E-cores (Icestorm, iPhone 12) + * showed ~27 % `Discarded` (bad-spec / mispredict) + * on the AS variant of graphql-validation under + * fast-interp, where megamorphic vtable dispatch + * hits CALL_INDIRECT thousands of times; the layout + * hint matters more than the branch hint on Apple's + * sophisticated predictor. PMU bucket shares stay + * within run-to-run noise on both Porffor and AS + * graphql-validation workloads, so the change is + * documentation-as-code more than a speedup — + * keep it because the cold-path semantic is real + * and the cost is zero. */ + if (__builtin_expect((uint32)val >= tbl_inst->cur_size, 0)) { wasm_set_exception(module, "undefined element"); goto got_exception; } @@ -1782,13 +1884,13 @@ wasm_interp_call_func_bytecode(WASMModuleInstance *module, /* clang-format off */ #if WASM_ENABLE_GC == 0 fidx = (uint32)tbl_inst->elems[val]; - if (fidx == (uint32)-1) { + if (__builtin_expect(fidx == (uint32)-1, 0)) { wasm_set_exception(module, "uninitialized element"); goto got_exception; } #else func_obj = (WASMFuncObjectRef)tbl_inst->elems[val]; - if (!func_obj) { + if (__builtin_expect(!func_obj, 0)) { wasm_set_exception(module, "uninitialized element"); goto got_exception; } @@ -1801,7 +1903,7 @@ wasm_interp_call_func_bytecode(WASMModuleInstance *module, * another module. in that case, we don't validate * the elem value while loading */ - if (fidx >= module->e->function_count) { + if (__builtin_expect(fidx >= module->e->function_count, 0)) { wasm_set_exception(module, "unknown function"); goto got_exception; } @@ -1814,14 +1916,16 @@ wasm_interp_call_func_bytecode(WASMModuleInstance *module, else cur_func_type = cur_func->u.func->func_type; - /* clang-format off */ + /* clang-format off */ #if WASM_ENABLE_GC == 0 - if (cur_type != cur_func_type) { + if (__builtin_expect(cur_type != cur_func_type, 0)) { wasm_set_exception(module, "indirect call type mismatch"); goto got_exception; } #else - if (!wasm_func_type_is_super_of(cur_type, cur_func_type)) { + if (__builtin_expect( + !wasm_func_type_is_super_of(cur_type, cur_func_type), + 0)) { wasm_set_exception(module, "indirect call type mismatch"); goto got_exception; } @@ -1836,14 +1940,406 @@ wasm_interp_call_func_bytecode(WASMModuleInstance *module, } #if WASM_ENABLE_EXCE_HANDLING != 0 + HANDLE_OP(WASM_OP_THROW) + { + /* Loader emits + * `WASM_OP_THROW + * + * ... + * `. Read the tag plus payload-source + * metadata, then walk the eh-stack in find_a_catch_handler to + * find a matching catch — first in this frame, then via + * return_func's hook in caller frames, and finally + * falling out to the host via got_exception when no + * match is found anywhere. + * + * Payload routing: when a same-function catch matches, + * find_a_catch_handler copies frame_lp[src[i]] into + * the catch's pre-allocated dst slots (recorded on + * `WASMFastEHCatch.param_dst_offsets` at load time). + * For tag-without-params (the typical Porffor shape), + * `throw_param_cell_num == 0` makes the copy a no-op. + * For cross-function dispatch the source frame is + * torn down before the caller's walker runs, so the + * payload is dropped — this gap is documented in + * AGENTS.md and exercised as + * `cross_function_tag_with_params` (#[ignore]). */ + exception_tag_index = read_uint32(frame_ip); + throw_param_cell_num = read_uint32(frame_ip); + throw_src_offsets = (int16 *)frame_ip; + frame_ip += sizeof(int16) * throw_param_cell_num; + goto find_a_catch_handler; + } + + find_a_catch_handler: + { + /* The eh-stack lives in the trailing cells of + * frame->operand[] (see call_func_from_entry and the + * runtime push from WASM_OP_TRY). Each entry packs the + * eh-table index into the low 31 bits; the top bit + * (EH_TRY_CATCH_STATE_BIT) is set on entries whose + * catch handler is *already running* — those are + * skipped here so a throw raised from inside a catch + * body propagates outward rather than re-entering the + * same handler. + * + * Cost shape: the walk runs only on the throw path + * (cold). CALL / LOAD / STORE handlers are untouched, + * and the eh-stack cells share a cache line with the + * value stack they're allocated next to, so the walk + * hits warm memory. + * + * Known limitation in this patch: try-regions with a + * non-void result-type are *not yet supported* by the + * normal-flow path. The fix is a loader-side + * try-body→block-dynamic-offset COPY emit at CATCH + * processing time (mirrors how WASM_OP_ELSE aligns + * the if-body's result via reserve_block_ret). See the + * AGENTS.md "Open follow-up — WAMR fast-interp legacy + * exception handling" section for the architectural + * note. The throw → catch dispatch implemented here + * still works correctly for void-result try-regions + * (which is what graphql-validation-porf-accurate's + * single try-block is). */ + WASMFunction *cur_wasm_func = cur_func->u.func; + uint32 *eh_stack = EH_STACK_BASE(frame_lp, cur_func, cur_wasm_func); + uint32 i; + for (i = frame->eh_count; i > 0; i--) { + uint32 *cells = eh_stack + (i - 1) * EH_ENTRY_CELLS; + uint32 packed = cells[0]; + uint32 eh_idx; + WASMFastEHEntry *entry; + uint32 j; + if (packed & EH_TRY_CATCH_STATE_BIT) + continue; /* in-progress catch — skip */ + eh_idx = packed & ~EH_TRY_CATCH_STATE_BIT; + bh_assert(eh_idx < cur_wasm_func->exception_handler_count); + entry = &cur_wasm_func->exception_handlers[eh_idx]; + if (entry->delegate_target_depth != UINT32_MAX) { + /* This try-region was closed by `delegate N`, + * not `end`. The spec says the exception is + * re-raised at the location of the target + * block — i.e. it propagates past every try + * whose body the delegate's try sits inside + * (but the target is also inside). The loader + * already counted those tries as + * `delegate_target_depth = delta`. Marking + * THIS entry as consumed and decrementing `i` + * by `delta` makes the for-loop's natural + * i-- land on the first eh-stack entry + * strictly *outside* the target block — which + * is exactly where the spec wants the throw + * to resume matching. + * + * If `delta + 1 >= i`, the target block is + * outside this function's eh-stack entirely + * (e.g. `delegate `): + * break out to the "no handler in this + * frame" path and let return_func forward the + * exception to the caller. + * + * Cost: cold path; only THROW reaches here. + * Hot ops untouched. */ + uint32 delta = entry->delegate_target_depth; + cells[0] = packed | EH_TRY_CATCH_STATE_BIT; + if (delta + 1 >= i) { + /* Underflow guard + escape signal: any + * `delta` that would skip past the start + * of the eh-stack means the target lies + * past this function's try-blocks. */ + break; + } + i -= delta; + continue; + } + for (j = 0; j < entry->catch_count; j++) { + if (entry->catches[j].tag_index == exception_tag_index) { + /* Mark the entry as in-progress catch and + * stash the tag that's being handled so a + * RETHROW from this catch body can re- + * raise it. */ + cells[0] = packed | EH_TRY_CATCH_STATE_BIT; + cells[1] = exception_tag_index; + /* Payload copy (same-function dispatch). + * The loader guaranteed + * `entry->catches[j].param_cell_num == + * throw_param_cell_num` by checking the + * tag type at both THROW and CATCH; the + * runtime just executes the cell-wise + * frame_lp move. Tag-without-params makes + * the loop trivial. */ + if (throw_param_cell_num > 0 + && entry->catches[j].param_dst_offsets) { + uint32 c; + int16 *dst = entry->catches[j].param_dst_offsets; + for (c = 0; c < throw_param_cell_num; c++) { + frame_lp[dst[c]] = + frame_lp[throw_src_offsets[c]]; + } + } + /* Pop the inner eh-stack entries that the + * throw is jumping past. When the match is + * at the topmost entry this is a no-op + * (i == frame->eh_count). When the match is + * an outer entry, the nested-try entries + * above it (indices i .. eh_count-1) are + * out of scope after the catch-dispatch; + * leaving them counted would let a + * subsequent throw inside the catch body + * see stale in-scope entries (and a tight + * loop of throw → outer-catch → throw + * would eventually overflow the fixed + * reservation). The matched entry stays + * at index i-1 with its state bit set; the + * catch body's END pops it when it + * completes. Cost: one indexed store on + * the cold throw path; CALL / LOAD / STORE + * untouched. */ + frame->eh_count = i; + frame_ip = entry->catches[j].handler_pc; + HANDLE_OP_END(); + } + } + if (entry->catch_all_pc) { + /* catch_all binds no payload (spec: catch_all + * has no exception values), so we drop the + * src cells here. RETHROW from inside a + * catch_all body cannot re-emit a payload — + * documented as a known limitation. */ + cells[0] = packed | EH_TRY_CATCH_STATE_BIT; + cells[1] = exception_tag_index; + /* Same unwind as the typed-catch path above — + * pop any nested-try entries the throw is + * jumping past so a subsequent throw inside + * this catch_all body doesn't dispatch + * against stale inner entries. */ + frame->eh_count = i; + frame_ip = entry->catch_all_pc; + HANDLE_OP_END(); + } + } + /* No handler in this frame. Hand the exception off to + * the caller via return_func, which checks + * frame->exception_raised after RECOVER_CONTEXT and + * re-enters this label with the caller's frame in + * scope. If we're already at the top of the wasm + * stack, the existing got_exception path lets the + * host observe the trap via wasm_runtime_get_exception. + * + * Tag-with-params payload is intentionally NOT + * preserved across the frame boundary: the source + * cells (throw_src_offsets) live in *this* frame's + * frame_lp, which return_func is about to tear down. + * A caller-side typed catch would then bind + * uninitialized destination slots, producing wrong + * results in the catch body (or, if the typed catch + * uses the slots as a struct-of-pointers, memory + * corruption). The safe action when a payload- + * bearing throw escapes its callee is to trap to the + * host with a clear diagnostic. Same-function + * payload routing (the common Porffor / AS shape) + * is unaffected — it dispatches via the loop above + * before this branch runs. catch_all in the caller + * would technically tolerate a zero-payload bind, + * but the typed-vs-catch_all choice happens in the + * caller's walker, which we can't peek into here + * without coupling the frames; trap unconditionally + * for payload-bearing throws and let the test + * `cross_function_tag_with_params` document the + * shape. */ + if (prev_frame && prev_frame->ip) { + if (throw_param_cell_num > 0) { + wasm_set_exception(module, + "cross-function exception payload " + "not supported by fast-interp"); + goto got_exception; + } + prev_frame->tag_index = exception_tag_index; + prev_frame->exception_raised = true; + goto return_func; + } + { + char exception_buf[64]; + snprintf(exception_buf, sizeof(exception_buf), + "wasm exception thrown (tag %u)", exception_tag_index); + wasm_set_exception(module, exception_buf); + } + goto got_exception; + } + HANDLE_OP(WASM_OP_TRY) + { + /* Loader emits `WASM_OP_TRY `. Push one + * entry onto the per-frame eh-stack so subsequent + * THROW / RETHROW handlers can find the in-scope + * catches by walking it. + * + * The eh-stack lives in the trailing cells of + * frame->operand[] — EH_ENTRY_CELLS cells per try- + * region, sized by + * cur_wasm_func->exception_handler_count * + * EH_ENTRY_CELLS at frame setup. Cell 1 (caught_tag) + * is unspecified while the entry is in TRY state and + * gets written by the throw walker on catch dispatch. + * Cost: one indexed store + one increment, both on a + * cold path; CALL / LOAD / STORE are untouched. */ + uint32 eh_idx = read_uint32(frame_ip); + WASMFunction *cur_wasm_func = cur_func->u.func; + uint32 *eh_stack = + EH_STACK_BASE(frame_lp, cur_func, cur_wasm_func); + bh_assert(frame->eh_count + < cur_wasm_func->exception_handler_count); + eh_stack[frame->eh_count * EH_ENTRY_CELLS + 0] = eh_idx; + frame->eh_count++; + HANDLE_OP_END(); + } + HANDLE_OP(WASM_OP_CATCH) - HANDLE_OP(WASM_OP_THROW) + HANDLE_OP(WASM_OP_CATCH_ALL) + { + /* Loader emits ` ` (commit 1's + * exception_handlers table records each catch body's + * pc and the region's end_of_region_pc). + * + * Reached via *normal flow* — execution either ran the + * try body to completion (CATCH is the first opcode + * after the try body) or fell through from a previous + * catch body. Either way: pop one eh-stack entry and + * branch past the try-region's end. The THROW dispatch + * (follow-up commit) jumps directly to a catch body's + * first opcode, *skipping* the CATCH opcode itself, so + * this handler never runs as a result of a caught + * throw — only as a fall-through exit. */ + uint32 eh_idx = read_uint32(frame_ip); + WASMFunction *cur_wasm_func = cur_func->u.func; + bh_assert(eh_idx < cur_wasm_func->exception_handler_count); + bh_assert(frame->eh_count > 0); + frame->eh_count--; + frame_ip = + cur_wasm_func->exception_handlers[eh_idx].end_of_region_pc; + HANDLE_OP_END(); + } + HANDLE_OP(WASM_OP_RETHROW) + { + /* Loader emits `WASM_OP_RETHROW `. Re-raise + * the exception currently being handled by an + * enclosing catch (the (depth+1)-th `state=CATCH` + * entry from the top of the eh-stack at this point — + * each in-progress catch we're nested in contributes + * one such entry, in source order). RETHROW is a + * cold op (only fires inside catch bodies); the walk + * runs across at most the number of catches nested + * around the rethrow site. CALL / LOAD / STORE are + * untouched. */ + uint32 depth = read_uint32(frame_ip); + WASMFunction *cur_wasm_func = cur_func->u.func; + uint32 *eh_stack = + EH_STACK_BASE(frame_lp, cur_func, cur_wasm_func); + uint32 i; + uint32 catch_seen = 0; + for (i = frame->eh_count; i > 0; i--) { + uint32 *cells = eh_stack + (i - 1) * EH_ENTRY_CELLS; + if (!(cells[0] & EH_TRY_CATCH_STATE_BIT)) + continue; + if (catch_seen == depth) { + /* Re-raise the caught tag against the *outer* + * try-regions. find_a_catch_handler iterates + * top-down and skips state=CATCH entries, so + * this same entry won't re-match. + * + * Payload routing: the original throw's + * payload values were copied into THIS + * catch's dst slots by the previous + * find_a_catch_handler dispatch. The wasm + * spec says the catch body can't mutate + * those exception values directly (they're + * not addressable as locals, and the only + * way to read them is to pop off the + * operand stack at catch entry — which + * advances past the dst slots without + * writing them back). So at RETHROW time + * the dst slots still hold the original + * payload, and we can point throw_src_offsets + * at them so the outer catch's copy lands + * on a fresh set of dst slots with the + * same values. + * + * If the original match was via catch_all + * (no typed catch matched cells[1]), + * `match->param_dst_offsets == NULL` and the + * payload was already dropped at the + * catch_all dispatch. RETHROW from + * catch_all then re-raises with no payload + * — documented as a known limitation. */ + uint32 ent_eh_idx = cells[0] & ~EH_TRY_CATCH_STATE_BIT; + WASMFastEHEntry *ent = + &cur_wasm_func->exception_handlers[ent_eh_idx]; + WASMFastEHCatch *match = NULL; + uint32 mj; + for (mj = 0; mj < ent->catch_count; mj++) { + if (ent->catches[mj].tag_index == cells[1]) { + match = &ent->catches[mj]; + break; + } + } + if (match && match->param_dst_offsets) { + throw_param_cell_num = match->param_cell_num; + throw_src_offsets = match->param_dst_offsets; + } + else { + throw_param_cell_num = 0; + throw_src_offsets = NULL; + } + exception_tag_index = cells[1]; + goto find_a_catch_handler; + } + catch_seen++; + } + /* Loader validated rethrow's depth at compile time; + * if we got here the eh-stack is inconsistent with + * the IR (typically a runtime bug in the loader's + * eh-table population). */ + wasm_set_exception(module, "rethrow depth out of range"); + goto got_exception; + } + HANDLE_OP(WASM_OP_DELEGATE) - HANDLE_OP(WASM_OP_CATCH_ALL) + { + /* Normal-flow exit from a `try ... delegate N` region: + * the try body completed without throwing, so the + * runtime just pops the eh-stack entry that + * HANDLE_OP(WASM_OP_TRY) pushed and falls through to + * the next op in the rewritten IR (which is whatever + * came after the `delegate N` in source). + * + * The forwarding semantics ("if the try body throws, + * re-raise at the target block") are handled by the + * find_a_catch_handler walker reading the eh-table + * entry's `delegate_target_depth` and skipping that + * many nested-try eh-stack entries — DELEGATE itself + * doesn't run in the throw path, only on fall-through. + * + * No immediate to read: the loader skipped emit_br_info + * so the depth lives in the per-function eh-table + * indexed by the eh_idx of *this* try-region (which is + * the eh-stack's top). Cost: one decrement on a cold + * path; CALL / LOAD / STORE untouched. */ + bh_assert(frame->eh_count > 0); + frame->eh_count--; + HANDLE_OP_END(); + } + HANDLE_OP(EXT_OP_TRY) { + /* The fast-interp loader doesn't emit EXT_OP_TRY yet + * (the eh-table records CATCH / CATCH_ALL / DELEGATE + * indices directly on the per-function table; TRY's + * uint32 immediate is the eh_idx, not a type-index + * blocktype). Reaching this handler means a future + * loader change started emitting EXT_OP_TRY without + * the runtime catching up — surface that as an + * explicit trap. */ wasm_set_exception(module, "unsupported opcode"); goto got_exception; } @@ -7526,9 +8022,31 @@ wasm_interp_call_func_bytecode(WASMModuleInstance *module, HANDLE_OP(WASM_OP_GET_LOCAL) HANDLE_OP(WASM_OP_DROP) HANDLE_OP(WASM_OP_DROP_64) +#if WASM_ENABLE_EXCE_HANDLING != 0 + HANDLE_OP(WASM_OP_END) + { + /* Block / loop / if / function-level `end` is stripped from + * the IR at load time (skip_label in the END case of + * wasm_loader_prepare_bytecode). Only try-region `end`s + * survive — the loader keeps them so the runtime can pop + * the matching eh-stack entry here when control falls + * through the bottom of a catch body (or runs the body of + * a catchless `try ... end`). + * + * Cost: one decrement on a cold path. CALL / LOAD / STORE + * are untouched. */ + bh_assert(frame->eh_count > 0); + frame->eh_count--; + HANDLE_OP_END(); + } + + HANDLE_OP(WASM_OP_BLOCK) + HANDLE_OP(WASM_OP_LOOP) +#else HANDLE_OP(WASM_OP_BLOCK) HANDLE_OP(WASM_OP_LOOP) HANDLE_OP(WASM_OP_END) +#endif HANDLE_OP(WASM_OP_NOP) HANDLE_OP(EXT_OP_BLOCK) HANDLE_OP(EXT_OP_LOOP) @@ -7734,6 +8252,17 @@ wasm_interp_call_func_bytecode(WASMModuleInstance *module, * these cells */ local_cell_num = cur_func->param_cell_num + cur_func->local_cell_num; +#endif +#if WASM_ENABLE_EXCE_HANDLING != 0 + /* EH_ENTRY_CELLS cells per try-region in the function, + * appended past the value stack — cell 0 holds the + * packed eh_idx | state_bit, cell 1 holds the caught tag + * for RETHROW. Functions without try blocks pay zero + * cells. Mirrors classic-interp's eh_size accounting at + * wasm_interp_classic.c:6786 (which also stores per- + * handler pointers on the value stack). */ + all_cell_num += + cur_wasm_func->exception_handler_count * EH_ENTRY_CELLS; #endif /* param_cell_num, local_cell_num, const_cell_num and max_stack_cell_num are all no larger than UINT16_MAX (checked @@ -7751,6 +8280,21 @@ wasm_interp_call_func_bytecode(WASMModuleInstance *module, frame_ip = wasm_get_func_code(cur_func); frame_ip_end = wasm_get_func_code_end(cur_func); +#if WASM_ENABLE_EXCE_HANDLING != 0 + /* eh-stack starts empty; WASM_OP_TRY appends entries. */ + frame->eh_count = 0; + /* exception_raised is the marker `return_func` reads on + * every wasm-to-wasm call return; if a callee's throw + * found no in-frame handler it stashes the tag on the + * caller's frame->tag_index and sets this flag, then + * goes to return_func. ALLOC_FRAME doesn't zero-init + * the frame header, so leaving the slot uninitialized + * trips the return_func hook on every call return with + * stale memory contents — turning a non-throwing run + * into "wasm exception thrown (tag N)" for random N. */ + frame->exception_raised = false; +#endif + frame_lp = frame->lp = frame->operand + cur_wasm_func->const_cell_num; @@ -7807,6 +8351,34 @@ wasm_interp_call_func_bytecode(WASMModuleInstance *module, RECOVER_CONTEXT(prev_frame); #if WASM_ENABLE_GC != 0 local_cell_num = cur_func->param_cell_num + cur_func->local_cell_num; +#endif +#if WASM_ENABLE_EXCE_HANDLING != 0 + /* Inter-function unwind: the callee stashed a wasm tag on + * this frame (now the active one after RECOVER_CONTEXT) + * when its eh-stack walk found no in-frame match. Re-enter + * find_a_catch_handler so the caller's eh-stack gets a + * chance to catch. Predicted strongly not-taken — + * exceptions are rare, this single check is the entire + * CALL-return-side cost of EH; the success path takes the + * HANDLE_OP_END() below. + * + * Cross-frame payload routing: the callee's throw site's + * source slots lived in the callee's frame_lp, which has + * already been freed by the time we get here. We zero out + * the throw_param_cell_num / throw_src_offsets pair so the + * caller's find_a_catch_handler doesn't try to dereference + * freed memory — the catch (if any matches) will fire with + * a zero-cell payload. This is the same gap documented at + * the WASM_OP_THROW handler and surfaced as + * `cross_function_tag_with_params` in the integration + * suite. */ + if (frame->exception_raised) { + exception_tag_index = frame->tag_index; + throw_param_cell_num = 0; + throw_src_offsets = NULL; + frame->exception_raised = false; + goto find_a_catch_handler; + } #endif HANDLE_OP_END(); } diff --git a/core/iwasm/interpreter/wasm_loader.c b/core/iwasm/interpreter/wasm_loader.c index a2c67bea2c..c51b29118a 100644 --- a/core/iwasm/interpreter/wasm_loader.c +++ b/core/iwasm/interpreter/wasm_loader.c @@ -737,7 +737,7 @@ load_init_expr(WASMModule *module, const uint8 **p_buf, const uint8 *buf_end, uint8 flag, *p_float; uint32 i; ConstExprContext const_expr_ctx = { 0 }; - WASMValue cur_value; + WASMValue cur_value = { 0 }; #if WASM_ENABLE_GC != 0 uint32 opcode1, type_idx; uint8 opcode; @@ -872,7 +872,8 @@ load_init_expr(WASMModule *module, const uint8 **p_buf, const uint8 *buf_end, { InitializerExpression *l_expr, *r_expr; - WASMValue l_value, r_value; + WASMValue l_value = { 0 }; + WASMValue r_value = { 0 }; uint8 l_flag, r_flag; uint8 value_type; @@ -1003,6 +1004,12 @@ load_init_expr(WASMModule *module, const uint8 **p_buf, const uint8 *buf_end, uint8 type1; CHECK_BUF(p, p_end, 1); type1 = read_uint8(p); + if (type1 != VALUE_TYPE_FUNCREF + && type1 != VALUE_TYPE_EXTERNREF) { + set_error_buf(error_buf, error_buf_size, + "invalid reference type"); + goto fail; + } cur_value.ref_index = NULL_REF; if (!push_const_expr_stack(&const_expr_ctx, flag, type1, &cur_value, @@ -1393,7 +1400,7 @@ load_init_expr(WASMModule *module, const uint8 **p_buf, const uint8 *buf_end, } else { /* WASM_OP_ARRAY_NEW_DEFAULT */ - WASMValue len_val; + WASMValue len_val = { 0 }; uint32 len; /* POP(i32) */ @@ -5681,6 +5688,7 @@ handle_branch_hint_section(const uint8 *buf, const uint8 *buf_end, goto fail; } + CHECK_BUF(buf, buf_end, 1); uint8 data = *buf++; if (data == 0x00) new_hint->is_likely = false; @@ -7360,6 +7368,33 @@ wasm_loader_unload(WASMModule *module) wasm_runtime_free(module->functions[i]->code_compiled); if (module->functions[i]->consts) wasm_runtime_free(module->functions[i]->consts); +#if WASM_ENABLE_EXCE_HANDLING != 0 + if (module->functions[i]->exception_handlers) { + uint32 eh_idx; + for (eh_idx = 0; + eh_idx < module->functions[i]->exception_handler_count; + eh_idx++) { + WASMFastEHEntry *eh_entry = + &module->functions[i]->exception_handlers[eh_idx]; + if (eh_entry->catches) { + uint32 cj; + /* Free each catch's tag-with-params dst + * slot array. param_dst_offsets is NULL + * for the (common) tag-without-params + * case, in which case the free is a + * no-op. */ + for (cj = 0; cj < eh_entry->catch_count; cj++) { + if (eh_entry->catches[cj].param_dst_offsets) { + wasm_runtime_free(eh_entry->catches[cj] + .param_dst_offsets); + } + } + wasm_runtime_free(eh_entry->catches); + } + } + wasm_runtime_free(module->functions[i]->exception_handlers); + } +#endif /* end of WASM_ENABLE_EXCE_HANDLING */ #endif #if WASM_ENABLE_FAST_JIT != 0 if (module->functions[i]->fast_jit_jitted_code) { @@ -8470,6 +8505,14 @@ typedef struct BranchBlock { * to copy the stack operands to the loop block's arguments in * wasm_loader_emit_br_info for opcode br. */ uint16 start_dynamic_offset; +#if WASM_ENABLE_EXCE_HANDLING != 0 + /* For LABEL_TYPE_TRY/CATCH/CATCH_ALL: index into + * func->exception_handlers (the same index across the whole try- + * catch-end region — a CATCH clause inherits its parent TRY's + * index when the loader rewrites the block label). UINT32_MAX + * for non-EH label types. */ + uint32 eh_entry_idx; +#endif #endif /* Indicate the operand stack is in polymorphic state. @@ -8551,6 +8594,13 @@ typedef struct WASMLoaderContext { * than the final code_compiled_size, we record the peak size to ensure * there will not be invalid memory access during second traverse */ uint32 code_compiled_peak_size; +#if WASM_ENABLE_EXCE_HANDLING != 0 + /* Index of the next entry to claim in func->exception_handlers, + * during the second traverse only (the first traverse merely counts + * try-blocks into func->exception_handler_count to size the array). + * Reset to 0 in wasm_loader_ctx_reinit. */ + uint32 cur_eh_entry_idx; +#endif #endif } WASMLoaderContext; @@ -8822,6 +8872,11 @@ wasm_loader_ctx_init(WASMFunction *func, char *error_buf, uint32 error_buf_size) #if WASM_ENABLE_EXCE_HANDLING != 0 func->exception_handler_count = 0; +#if WASM_ENABLE_FAST_INTERP != 0 + /* Allocated at the start of the second traverse, once + * exception_handler_count is known from the first traverse. */ + func->exception_handlers = NULL; +#endif #endif #if WASM_ENABLE_FAST_INTERP != 0 @@ -9344,6 +9399,12 @@ wasm_loader_push_frame_csp(WASMLoaderContext *ctx, uint8 label_type, #if WASM_ENABLE_FAST_INTERP != 0 ctx->frame_csp->dynamic_offset = ctx->dynamic_offset; ctx->frame_csp->patch_list = NULL; +#if WASM_ENABLE_EXCE_HANDLING != 0 + /* Default sentinel; the WASM_OP_TRY handler patches this on entry + * and the CATCH/CATCH_ALL handlers propagate it onto the rewritten + * label. */ + ctx->frame_csp->eh_entry_idx = UINT32_MAX; +#endif #endif ctx->frame_csp++; ctx->csp_num++; @@ -9567,6 +9628,13 @@ wasm_loader_ctx_reinit(WASMLoaderContext *ctx) /* init preserved local offsets */ ctx->preserved_local_offset = ctx->max_dynamic_offset; +#if WASM_ENABLE_EXCE_HANDLING != 0 + /* Start of the second traverse — reset the per-function try-block + * cursor so it tracks the same source-order index as the first + * traverse used to size func->exception_handlers. */ + ctx->cur_eh_entry_idx = 0; +#endif + /* const buf is reserved */ return true; } @@ -11247,6 +11315,66 @@ check_branch_block(WASMLoaderContext *loader_ctx, uint8 **p_buf, uint8 *buf_end, } #if WASM_ENABLE_EXCE_HANDLING != 0 +/* Returns the number of LABEL_TYPE_TRY / _CATCH / _CATCH_ALL + * frames whose END the runtime br will SKIP — i.e. the count of + * such frames at csp positions `cur_block` down to `target_block` + * inclusive (target_block included because br to a non-LOOP + * target lands AFTER target's end, skipping it; LOOP targets + * aren't try-typed so the inclusive vs exclusive distinction + * doesn't matter for them). The runtime br jumps directly to the + * target's resolved pc without decrementing `frame->eh_count`, + * so each such frame represents one stale eh-stack entry that + * survives the br. A single leaked entry is benign — frame + * allocation reserves `exception_handler_count * EH_ENTRY_CELLS` + * cells, the walker iterates top-down so sibling-try throws + * still match correctly, and the stale entry dies at frame + * teardown. But a br to a surrounding LOOP re-pushes one entry + * every iteration, eventually overflowing the static reservation; + * the resulting out-of-bounds writes go through silently in + * release builds (`bh_assert` is a no-op without `BH_DEBUG`). + * Caller logs a warning so the shape shows up in load-time + * diagnostics. */ +static uint32 +count_try_blocks_crossed(BranchBlock *cur_block, BranchBlock *target_block) +{ + BranchBlock *b; + uint32 count = 0; + for (b = cur_block; b >= target_block; b--) { + if (b->label_type == LABEL_TYPE_TRY || b->label_type == LABEL_TYPE_CATCH + || b->label_type == LABEL_TYPE_CATCH_ALL) { + count++; + } + } + return count; +} + +/* A try-crossing br only leaks one eh-stack entry harmlessly when it + * executes once. If the br (or its target) is enclosed by a LOOP that + * can re-execute it, every iteration re-pushes a TRY entry and the + * leaked entries eventually overrun the static + * `exception_handler_count * EH_ENTRY_CELLS` reservation. + * + * The direct case (br target IS a loop entry) is handled separately by + * the caller. This walks the control frames the br does NOT exit — the + * target frame `target_block` and everything below it down to the + * function frame — looking for an enclosing LOOP. Such a loop keeps the + * br live after it lands, so the per-iteration leak applies. + * + * Returns true if a try-crossing br landing at `target_block` would + * leak per loop iteration via an enclosing loop. */ +static bool +br_try_leak_in_enclosing_loop(BranchBlock *frame_csp_bottom, + BranchBlock *target_block) +{ + BranchBlock *b; + for (b = target_block; b >= frame_csp_bottom; b--) { + if (b->label_type == LABEL_TYPE_LOOP) { + return true; + } + } + return false; +} + static BranchBlock * check_branch_block_for_delegate(WASMLoaderContext *loader_ctx, uint8 **p_buf, uint8 *buf_end, char *error_buf, @@ -11961,6 +12089,27 @@ wasm_loader_prepare_bytecode(WASMModule *module, WASMFunction *func, loader_ctx->i32_const_num = k; } } + +#if WASM_ENABLE_EXCE_HANDLING != 0 + /* The first traverse counted `func->exception_handler_count` + * try-blocks; the second traverse is about to populate one + * entry per try-block in source order. Allocate the array now + * (zero-initialized) and reset delegate_target_depth to the + * "no delegate" sentinel on every entry. */ + if (func->exception_handler_count > 0) { + uint64 eh_size = + (uint64)sizeof(WASMFastEHEntry) * func->exception_handler_count; + uint32 eh_i; + if (!(func->exception_handlers = + loader_malloc(eh_size, error_buf, error_buf_size))) { + goto fail; + } + for (eh_i = 0; eh_i < func->exception_handler_count; eh_i++) { + func->exception_handlers[eh_i].delegate_target_depth = + UINT32_MAX; + } + } +#endif } #endif @@ -12011,11 +12160,17 @@ wasm_loader_prepare_bytecode(WASMModule *module, WASMFunction *func, #if WASM_ENABLE_EXCE_HANDLING != 0 case WASM_OP_TRY: if (opcode == WASM_OP_TRY) { - /* - * keep track of exception handlers to account for - * memory allocation - */ +#if WASM_ENABLE_FAST_INTERP != 0 + /* Two-traverse loader: the first traverse counts + * try-blocks into func->exception_handler_count so + * the second traverse can allocate the per-function + * exception_handlers[] table (see re_scan block). */ + if (loader_ctx->p_code_compiled == NULL) + func->exception_handler_count++; +#else + /* Single-traverse classic-interp / shared loader. */ func->exception_handler_count++; +#endif /* * try is a block @@ -12276,7 +12431,22 @@ wasm_loader_prepare_bytecode(WASMModule *module, WASMFunction *func, } #if WASM_ENABLE_EXCE_HANDLING != 0 else if (opcode == WASM_OP_TRY) { - skip_label(); + /* The auto-emit_label at the top of the dispatch + * loop already wrote the WASM_OP_TRY byte into the + * rewritten IR; the runtime handler for that + * opcode (HANDLE_OP(WASM_OP_TRY) in + * wasm_interp_fast.c) reads the uint32 eh_idx + * immediate we emit below and pushes one entry + * onto the per-frame eh-stack. Unlike BLOCK / LOOP, + * we keep the opcode in the IR — its runtime + * effect (push) is what makes throws find the + * right catches. */ + bh_assert(loader_ctx->cur_eh_entry_idx + < func->exception_handler_count); + (loader_ctx->frame_csp - 1)->eh_entry_idx = + loader_ctx->cur_eh_entry_idx; + emit_uint32(loader_ctx, loader_ctx->cur_eh_entry_idx); + loader_ctx->cur_eh_entry_idx++; } #endif else if (opcode == WASM_OP_IF) { @@ -12379,6 +12549,99 @@ wasm_loader_prepare_bytecode(WASMModule *module, WASMFunction *func, goto fail; } +#if WASM_ENABLE_FAST_INTERP != 0 + /* Fast-interp THROW IR shape (emitted in BOTH traverses + * so pass-1 / pass-2 size accounting stays balanced): + * + * + * + * + * ... + * + * Where `param_cell_num` is the sum across all params' + * cell widths (i32 = 1, i64 = 2, v128 = 4, etc.) and + * src_offset_i is the throw-site's frame_lp slot for + * the i-th payload cell, read directly off the top of + * `loader_ctx->frame_offset[]`. The validation loop + * below pops frame_ref / available_stack_cell but + * doesn't touch frame_offset, so the src offsets are + * stable to read here. They get consumed at runtime + * by find_a_catch_handler when a *same-function* + * catch matches: it copies `param_cell_num` cells + * from frame_lp[src_offset_i] into the catch body's + * `param_dst_offsets[i]` slots before jumping to + * handler_pc. + * + * Cross-function dispatch (callee throws, caller's + * catch fires after return_func unwinds) does NOT + * preserve the payload — the source slots live in a + * frame that's about to be torn down. That gap is + * documented as an ignored integration test, in line + * with the cost-model rule that EH must not tax hot + * ops: a per-thread payload buffer would force every + * CALL / RETURN handler to spill scratch state across + * the boundary, which we explicitly refuse. + * + * Tag-without-params is the common case (Porffor + * emits empty payloads; many spec tests use bare + * tags too). param_cell_num=0 makes the for-loop + * trivial and the resulting IR is just the tag_index + * + a single zero — same hot-path cost as the + * pre-tag-with-params shape, since the runtime + * read_uint32 of param_cell_num happens on the cold + * THROW handler. */ + emit_uint32(loader_ctx, tag_index); + emit_uint32(loader_ctx, tag_type->param_cell_num); + { + /* Multi-cell types (i64, f64, v128) only have a + * meaningful first-cell offset in + * `frame_offset[]` — subsequent cells of the + * same value are left uninitialized by + * `wasm_loader_push_frame_offset` (it just + * advances the pointer without writing). For + * each param walk the per-param first cell out + * of frame_offset and synthesize consecutive + * cell offsets `(first, first+1, ...)`; that + * matches the runtime invariant that an n-cell + * value occupies n consecutive frame_lp cells. + * + * In stack-polymorphic code (after an + * `unreachable`, e.g. `unreachable; throw $tag`) + * the offset stack does NOT actually hold the + * tag's params, so reading + * `frame_offset - param_cell_num` would underflow + * the offset stack. The validation loop below + * tolerates the missing values via the same + * polymorphic short-circuit used by the + * block-param path; mirror it here by emitting + * zeroed placeholder source offsets, which keeps + * pass-1 / pass-2 size accounting balanced while + * avoiding the underflow (this THROW is + * unreachable at runtime anyway). */ + uint32 pi, c, cell_so_far = 0; + int32 throw_avail_cell = + (int32)(loader_ctx->stack_cell_num + - cur_block->stack_cell_num); + bool stack_has_params = + !(throw_avail_cell < (int32)tag_type->param_cell_num + && cur_block->is_stack_polymorphic); + int16 *base = + loader_ctx->frame_offset - tag_type->param_cell_num; + for (pi = 0; pi < tag_type->param_count; pi++) { + uint32 this_cells = + wasm_value_type_cell_num(tag_type->types[pi]); + int16 first_slot = + stack_has_params ? base[cell_so_far] : 0; + for (c = 0; c < this_cells; c++) { + emit_operand( + loader_ctx, + stack_has_params ? (int16)(first_slot + c) : 0); + } + cell_so_far += this_cells; + } + } +#endif + int32 available_stack_cell = (int32)(loader_ctx->stack_cell_num - cur_block->stack_cell_num); @@ -12450,21 +12713,50 @@ wasm_loader_prepare_bytecode(WASMModule *module, WASMFunction *func, } case WASM_OP_RETHROW: { - /* must be done before checking branch block */ + /* must be done before reading the depth */ SET_CUR_BLOCK_STACK_POLYMORPHIC_STATE(true); - /* check the target catching block: LABEL_TYPE_CATCH */ - if (!(frame_csp_tmp = - check_branch_block(loader_ctx, &p, p_end, opcode, - error_buf, error_buf_size))) - goto fail; - - if (frame_csp_tmp->label_type != LABEL_TYPE_CATCH - && frame_csp_tmp->label_type != LABEL_TYPE_CATCH_ALL) { - /* trap according to spectest (rethrow.wast) */ - set_error_buf(error_buf, error_buf_size, - "invalid rethrow label"); - goto fail; + /* Manual depth + label-type validation. We deliberately + * skip the shared `check_branch_block` here because + * RETHROW doesn't *branch* to its target — it walks + * the eh-stack at runtime and re-raises — so the + * branch-info bytes that check_branch_block / + * emit_br_info would write between the auto-emitted + * opcode label and our depth immediate are dead + * weight (4 bytes arity + 8 bytes target ptr + + * arity-dependent operand-offsets, all unread by the + * runtime walker). Worse, leaving them in the IR + * shifts our depth immediate past where the runtime + * read_uint32(frame_ip) looks for it. */ + { + uint32 rethrow_depth = 0; + BranchBlock *target_block; + pb_read_leb_uint32(p, p_end, rethrow_depth); + if (rethrow_depth + 1 > loader_ctx->csp_num) { +#if WASM_ENABLE_SPEC_TEST == 0 + set_error_buf(error_buf, error_buf_size, + "unknown rethrow label"); +#else + set_error_buf(error_buf, error_buf_size, + "unknown label"); +#endif + goto fail; + } + target_block = loader_ctx->frame_csp - rethrow_depth - 1; + if (target_block->label_type != LABEL_TYPE_CATCH + && target_block->label_type != LABEL_TYPE_CATCH_ALL) { + /* trap according to spectest (rethrow.wast) */ + set_error_buf(error_buf, error_buf_size, + "invalid rethrow label"); + goto fail; + } +#if WASM_ENABLE_FAST_INTERP != 0 + /* Emit the depth as a uint32 immediate after the + * auto-emitted RETHROW opcode. Pass 1's size + * accounting must match pass 2's actual emit so + * we run this branch in both traverses. */ + emit_uint32(loader_ctx, rethrow_depth); +#endif } BranchBlock *cur_block = loader_ctx->frame_csp - 1; @@ -12476,15 +12768,95 @@ wasm_loader_prepare_bytecode(WASMModule *module, WASMFunction *func, } case WASM_OP_DELEGATE: { - /* check target block is valid */ - if (!(frame_csp_tmp = check_branch_block_for_delegate( - loader_ctx, &p, p_end, error_buf, error_buf_size))) - goto fail; - + /* Manual depth + label-type validation. Like RETHROW + * (above), we deliberately skip the shared + * `check_branch_block_for_delegate` here because: + * (1) DELEGATE doesn't *branch* to its target at + * runtime — when the try-body throws, the + * find_a_catch_handler walker reads the precomputed + * `delegate_target_depth` off the eh-table entry + * and skips the right number of nested-try entries + * on the per-frame eh-stack. The branch-info bytes + * that `emit_br_info` would write between the + * auto-emitted DELEGATE label and any subsequent + * operand are dead weight (4 bytes arity + 8 bytes + * target ptr, all unread by either the runtime + * DELEGATE handler or the throw walker). + * (2) Worse, leaving them in the IR shifts any + * immediate we *do* want to emit past where the + * runtime reads it — same gotcha that bit + * RETHROW. + * + * `delegate N` targets the (N+1)-th block out from the + * current try-delegate frame. The try-delegate itself + * still sits on the loader's csp stack at this point + * (POP_CSP is called below), so the target is at + * frame_csp - N - 2 + * and the spec rejects `delegate N` whose N+1 would + * climb past the function frame. */ + uint32 delegate_depth = 0; BranchBlock *cur_block = loader_ctx->frame_csp - 1; + BranchBlock *target_block; uint8 label_type = cur_block->label_type; - (void)label_type; + + pb_read_leb_uint32(p, p_end, delegate_depth); + bh_assert(loader_ctx->csp_num > 0); + if (loader_ctx->csp_num - 1 <= delegate_depth) { +#if WASM_ENABLE_SPEC_TEST == 0 + set_error_buf(error_buf, error_buf_size, + "unknown delegate label"); +#else + set_error_buf(error_buf, error_buf_size, "unknown label"); +#endif + goto fail; + } + target_block = loader_ctx->frame_csp - delegate_depth - 2; + (void)target_block; + +#if WASM_ENABLE_FAST_INTERP != 0 + /* Second traverse only: populate the eh-table entry so + * the runtime walker can dispatch through it. + * + * delegate_target_depth = (count of try / catch / + * catch_all blocks STRICTLY between cur_block and + * target_block on the loader's csp stack) + * + * At runtime those `delta` blocks are exactly the + * eh-stack entries immediately below the delegate's own + * entry that the throw walker must SKIP — the spec + * re-raises the exception "at the target block's + * location", so any try whose body the delegate's try + * is nested inside (but the target is also inside) + * doesn't get to catch it. + * + * end_of_region_pc still gets set to the IR pc just + * after the auto-emitted DELEGATE label. The walker + * never reads it for delegate entries (it forwards via + * delta instead), but a future DELEGATE-end runtime + * handler that wanted to advance frame_ip past the + * region could use it; recording it keeps the + * shape identical to the END(try) capture and the + * field semantics easy to reason about. */ + if (loader_ctx->p_code_compiled != NULL) { + uint32 eh_idx = cur_block->eh_entry_idx; + uint32 delta = 0; + BranchBlock *b; + bh_assert(eh_idx < func->exception_handler_count); + bh_assert(func->exception_handlers != NULL); + for (b = cur_block - 1; b > target_block; b--) { + if (b->label_type == LABEL_TYPE_TRY + || b->label_type == LABEL_TYPE_CATCH + || b->label_type == LABEL_TYPE_CATCH_ALL) { + delta++; + } + } + func->exception_handlers[eh_idx].delegate_target_depth = + delta; + func->exception_handlers[eh_idx].end_of_region_pc = + loader_ctx->p_code_compiled; + } +#endif /* DELEGATE ends the block */ POP_CSP(); break; @@ -12533,6 +12905,106 @@ wasm_loader_prepare_bytecode(WASMModule *module, WASMFunction *func, goto fail; } + /* Validate previous body's stack (try body on first + * CATCH, previous catch body on subsequent CATCH) + * matches the block's result type. Without this the + * loader would silently accept stack-shape mismatches + * between the try body and the catch bodies and the + * next op would read garbage. Same pattern as ELSE + * runs `check_block_stack` on the if-body before the + * else body's PUSH_TYPE sequence. */ + if (!check_block_stack(loader_ctx, cur_block, error_buf, + error_buf_size)) + goto fail; + +#if WASM_ENABLE_FAST_INTERP != 0 + /* For result-typed try-regions, inject a COPY of the + * previous body's last value(s) into the block's + * `dynamic_offset` slot BEFORE the auto-emitted CATCH + * label. The normal-flow CATCH dispatch jumps from + * here to `end_of_region_pc` — the body's value would + * otherwise be lost. Mirrors how `reserve_block_ret` + * + `case WASM_OP_ELSE` align the if-body's result + * for the else-body's END to read. Layout becomes: + * + * [previous body ops...] + * [EXT_OP_COPY_STACK_TOP src=prev_top dst=dyn_off] + * [CATCH label][eh_idx][dst-slots from PUSH...] + * [catch body ops...] + * + * The `src != dst` check runs in BOTH traverses so + * pass-1 size accounting matches pass-2 writes: + * `dynamic_offset` evolves identically in both + * passes, and although const-pool slots get + * renumbered between passes by the qsort/dedup at + * the start of pass 2, they stay strictly negative + * (offsets `-(count)..-1`) while `dynamic_offset` is + * strictly non-negative (`>= start_dynamic_offset = + * param_cell_num + local_cell_num`). So the + * predicate is sign-stable across passes. + * + * Multi-return-value try-regions need + * `EXT_OP_COPY_STACK_VALUES`; we error out + * explicitly until a follow-up commit lifts that + * restriction. Single-return covers every shape + * Porffor / AS / our integration tests emit. */ + { + uint8 *return_types = NULL; +#if WASM_ENABLE_GC == 0 + uint32 return_count = block_type_get_result_types( + &cur_block->block_type, &return_types); +#else + WASMRefTypeMap *return_reftype_maps = NULL; + uint32 return_reftype_map_count = 0; + uint32 return_count = block_type_get_result_types( + &cur_block->block_type, &return_types, + &return_reftype_maps, &return_reftype_map_count); +#endif + if (return_count == 1) { + uint8 cell = + (uint8)wasm_value_type_cell_num(return_types[0]); + int16 src = *(loader_ctx->frame_offset - cell); + int16 dst = cur_block->dynamic_offset; + if (src != dst) { + skip_label(); +#if WASM_ENABLE_SIMDE != 0 + if (cell == 4) { + emit_label(EXT_OP_COPY_STACK_TOP_V128); + } + else +#endif + if (cell == 2) { + emit_label(EXT_OP_COPY_STACK_TOP_I64); + } + else { + emit_label(EXT_OP_COPY_STACK_TOP); + } + emit_operand(loader_ctx, src); + emit_operand(loader_ctx, dst); + emit_label(opcode); + } + } + else if (return_count > 1) { + set_error_buf(error_buf, error_buf_size, + "multi-return try-region not " + "supported in fast interpreter"); + goto fail; + } + } + + /* Emit `` after the auto-emitted CATCH + * opcode. The runtime CATCH handler reads it to find + * end_of_region_pc when the catch is reached via + * normal flow. Emitted in BOTH traverses so pass 1's + * size measurement and pass 2's actual writes match; + * if this were inside the populate guard below, + * pass 2 would overrun the code_compiled buffer by + * sizeof(uint32) bytes per catch, corrupting whatever + * loader allocation the heap placed immediately after + * (typically func->exception_handlers itself). */ + emit_uint32(loader_ctx, cur_block->eh_entry_idx); +#endif + /* * replace frame_csp by LABEL_TYPE_CATCH */ @@ -12541,13 +13013,52 @@ wasm_loader_prepare_bytecode(WASMModule *module, WASMFunction *func, /* RESET_STACK removes the values pushed in TRY or previous * CATCH Blocks */ RESET_STACK(); + /* Reset the polymorphic flag the way `WASM_OP_ELSE` + * does: the catch body is a freshly-reachable region, + * not a continuation of the (dead) try body after a + * throw. Without this reset, the catch body's END + * runs `check_block_stack` in polymorphic mode, which + * emits a `POP_OFFSET_TYPE` operand byte for each + * return-cell — those bytes land between the auto- + * emitted END label and the case body's + * `skip_label()`, shifting the re-emitted END label + * forward by `2 * return_cell_num` bytes and leaving + * a corrupt handler-ptr at the originally-recorded + * `handler_pc`. (The same bug latent in non-EH + * polymorphic blocks doesn't bite because their END + * gets stripped from the IR entirely; the EH path's + * runtime needs the END opcode to actually exist for + * the eh-stack pop.) */ + SET_CUR_BLOCK_STACK_POLYMORPHIC_STATE(false); #if WASM_ENABLE_GC != 0 WASMRefType *ref_type; uint32 j = 0; #endif - /* push types on the stack according to caught type */ + /* Push the tag's params onto the catch body's operand + * stack. Classic-interp uses PUSH_TYPE (which only + * touches the value-type stack used by validation); + * fast-interp also needs `PUSH_OFFSET_TYPE`, which + * allocates fresh `dynamic_offset` slots for each cell + * (and emits the slot offsets as `int16` operands in + * the IR right after the eh_idx). The catch body's + * downstream ops then `POP_OFFSET_TYPE` to consume + * these slots — same shape the loader uses for + * block-with-params (see `copy_params_to_dynamic_ + * space`). + * + * Note: the emitted dst slots are *unused* by the + * runtime CATCH normal-flow handler (it only reads + * eh_idx and branches to end_of_region_pc) — they + * sit in the IR as dead bytes on the fall-through + * path. The throw walker doesn't read them either; + * it consults the pre-decoded copy on + * `WASMFastEHCatch.param_dst_offsets` (populated + * below). They're emitted only so PUSH_OFFSET_TYPE's + * pass-1 / pass-2 size accounting stays balanced and + * the catch body's POP_OFFSET_TYPEs find the right + * slot offsets in `frame_offset[]`. */ for (i = 0; i < func_type->param_count; i++) { #if WASM_ENABLE_GC != 0 if (wasm_is_type_multi_byte_type(func_type->types[i])) { @@ -12558,9 +13069,113 @@ wasm_loader_prepare_bytecode(WASMModule *module, WASMFunction *func, wasm_reftype_struct_size(ref_type)); j++; } +#endif + /* Allocate a fresh `dynamic_offset` slot for the + * catch param AND push its type onto `frame_ref` + * (so `stack_cell_num` stays balanced). One + * without the other doesn't work: a bare + * `PUSH_OFFSET_TYPE` leaves the offset side + * ahead of the ref side, so the catch body's + * first consumer (e.g. `global.set $g`) hits + * `wasm_loader_pop_frame_offset`'s polymorphic + * short-circuit — the CATCH block inherits the + * polymorphic flag from THROW's + * `SET_CUR_BLOCK_STACK_POLYMORPHIC_STATE`, and + * with `available_stack_cell == 0` the pop + * silently returns without emitting the source + * slot. The consumer's runtime read then lands + * on heap garbage and crashes with SIGBUS / + * SIGSEGV. PUSH_TYPE rebalances and avoids + * the short-circuit so the catch body's pops + * emit real source-slot operand bytes. */ +#if WASM_ENABLE_FAST_INTERP != 0 + PUSH_OFFSET_TYPE(func_type->types[i]); #endif PUSH_TYPE(func_type->types[i]); } + +#if WASM_ENABLE_FAST_INTERP != 0 + /* Second traverse only: append a fully-populated + * `WASMFastEHCatch` entry to the parent try-region's + * catches[]. handler_pc is captured *after* the + * PUSH_OFFSET_TYPE emits above so it points at the + * first rewritten-IR byte of the catch body proper + * (skipping the dead dst-slot bytes). param_cell_num + * is the sum of cells across all tag params (i32 = 1 + * cell, i64 = 2, v128 = 4); param_dst_offsets is a + * loader-owned copy of the int16 slot offsets just + * pushed onto frame_offset[]. NULL when the tag has + * no params (the typical Porffor shape). */ + if (loader_ctx->p_code_compiled != NULL) { + uint32 eh_idx = cur_block->eh_entry_idx; + WASMFastEHEntry *entry; + WASMFastEHCatch *new_catches; + uint64 new_size; + bh_assert(eh_idx < func->exception_handler_count); + bh_assert(func->exception_handlers != NULL); + entry = &func->exception_handlers[eh_idx]; + new_size = (uint64)sizeof(WASMFastEHCatch) + * (entry->catch_count + 1); + if (!(new_catches = loader_malloc(new_size, error_buf, + error_buf_size))) { + goto fail; + } + if (entry->catches) { + bh_memcpy_s(new_catches, (uint32)new_size, + entry->catches, + (uint32)sizeof(WASMFastEHCatch) + * entry->catch_count); + wasm_runtime_free(entry->catches); + } + new_catches[entry->catch_count].tag_index = tag_index; + new_catches[entry->catch_count].handler_pc = + loader_ctx->p_code_compiled; + new_catches[entry->catch_count].param_cell_num = + func_type->param_cell_num; + new_catches[entry->catch_count].param_dst_offsets = NULL; + if (func_type->param_cell_num > 0) { + uint64 dst_size = + (uint64)sizeof(int16) * func_type->param_cell_num; + int16 *dst; + uint32 pi, c, cell_so_far = 0; + int16 *base; + if (!(dst = loader_malloc(dst_size, error_buf, + error_buf_size))) { + wasm_runtime_free(new_catches); + goto fail; + } + /* Synthesize per-cell dst offsets from each + * param's first cell. Same multi-cell shape + * concern as the THROW src emit: + * `wasm_loader_push_frame_offset` writes a + * meaningful int16 only for the first cell + * of a multi-cell value (i64 / f64 / v128); + * subsequent cells of the same value have + * unspecified frame_offset entries. The + * runtime walker copies one frame_lp cell + * per iteration, so its `param_cell_num` + * loop needs an offset array indexed by + * absolute cell number, not by frame_offset + * position. Build that here by walking + * params and synthesizing `(first, first+1, + * ..., first+param_cells-1)` for each one. */ + base = loader_ctx->frame_offset + - func_type->param_cell_num; + for (pi = 0; pi < func_type->param_count; pi++) { + uint32 this_cells = + wasm_value_type_cell_num(func_type->types[pi]); + int16 first_slot = base[cell_so_far]; + for (c = 0; c < this_cells; c++) { + dst[cell_so_far + c] = (int16)(first_slot + c); + } + cell_so_far += this_cells; + } + new_catches[entry->catch_count].param_dst_offsets = dst; + } + entry->catches = new_catches; + entry->catch_count++; + } +#endif break; } case WASM_OP_CATCH_ALL: @@ -12576,6 +13191,83 @@ wasm_loader_prepare_bytecode(WASMModule *module, WASMFunction *func, goto fail; } + /* Same previous-body-stack validation as in CATCH. */ + if (!check_block_stack(loader_ctx, cur_block, error_buf, + error_buf_size)) + goto fail; + +#if WASM_ENABLE_FAST_INTERP != 0 + /* Same COPY-to-block-dynamic_offset shape as CATCH + * (see the long comment in the CATCH case for the + * rationale and pass-1/pass-2 alignment argument). + * catch_all is the only place the body-COPY can run + * for a try with a result-type and only a catch_all, + * so without this emit a result-typed + * `try (result T) ... catch_all` would lose the try + * body's value on the normal-flow path. */ + { + uint8 *return_types = NULL; +#if WASM_ENABLE_GC == 0 + uint32 return_count = block_type_get_result_types( + &cur_block->block_type, &return_types); +#else + WASMRefTypeMap *return_reftype_maps = NULL; + uint32 return_reftype_map_count = 0; + uint32 return_count = block_type_get_result_types( + &cur_block->block_type, &return_types, + &return_reftype_maps, &return_reftype_map_count); +#endif + if (return_count == 1) { + uint8 cell = + (uint8)wasm_value_type_cell_num(return_types[0]); + int16 src = *(loader_ctx->frame_offset - cell); + int16 dst = cur_block->dynamic_offset; + if (src != dst) { + skip_label(); +#if WASM_ENABLE_SIMDE != 0 + if (cell == 4) { + emit_label(EXT_OP_COPY_STACK_TOP_V128); + } + else +#endif + if (cell == 2) { + emit_label(EXT_OP_COPY_STACK_TOP_I64); + } + else { + emit_label(EXT_OP_COPY_STACK_TOP); + } + emit_operand(loader_ctx, src); + emit_operand(loader_ctx, dst); + emit_label(opcode); + } + } + else if (return_count > 1) { + set_error_buf(error_buf, error_buf_size, + "multi-return try-region not " + "supported in fast interpreter"); + goto fail; + } + } + + /* Emit `` after the auto-emitted CATCH_ALL + * opcode in BOTH traverses (pass 1's size accounting + * must include this or pass 2 overruns + * code_compiled). Pass 2 additionally records + * catch_all_pc on the parent try-region — set exactly + * once per region (spec allows at most one catch_all + * per try). */ + emit_uint32(loader_ctx, cur_block->eh_entry_idx); + if (loader_ctx->p_code_compiled != NULL) { + uint32 eh_idx = cur_block->eh_entry_idx; + bh_assert(eh_idx < func->exception_handler_count); + bh_assert(func->exception_handlers != NULL); + bh_assert(func->exception_handlers[eh_idx].catch_all_pc + == NULL); + func->exception_handlers[eh_idx].catch_all_pc = + loader_ctx->p_code_compiled; + } +#endif + /* no immediates */ /* replace frame_csp by LABEL_TYPE_CATCH_ALL */ cur_block->label_type = LABEL_TYPE_CATCH_ALL; @@ -12583,6 +13275,9 @@ wasm_loader_prepare_bytecode(WASMModule *module, WASMFunction *func, /* RESET_STACK removes the values pushed in TRY or previous * CATCH Blocks */ RESET_STACK(); + /* Same polymorphic reset as `WASM_OP_CATCH` — see the + * matching comment there for the rationale. */ + SET_CUR_BLOCK_STACK_POLYMORPHIC_STATE(false); /* catch_all has no tagtype and therefore no parameters */ break; @@ -12659,6 +13354,22 @@ wasm_loader_prepare_bytecode(WASMModule *module, WASMFunction *func, case WASM_OP_END: { BranchBlock *cur_block = loader_ctx->frame_csp - 1; +#if WASM_ENABLE_FAST_INTERP != 0 && WASM_ENABLE_EXCE_HANDLING != 0 + /* If this END closes a try-region (LABEL_TYPE_TRY when + * the region has only a try-body and no catch, or + * LABEL_TYPE_CATCH / CATCH_ALL when at least one catch + * clause is present), we need to remember the entry's + * index and label type now — POP_CSP and the subsequent + * skip_label / reserve_block_ret happen first, but the + * end_of_region_pc capture has to wait until after + * those advance loader_ctx->p_code_compiled. */ + uint32 ending_eh_idx = cur_block->eh_entry_idx; + bool ending_was_eh = + (ending_eh_idx != UINT32_MAX) + && (cur_block->label_type == LABEL_TYPE_TRY + || cur_block->label_type == LABEL_TYPE_CATCH + || cur_block->label_type == LABEL_TYPE_CATCH_ALL); +#endif /* check whether block stack matches its result type */ if (!check_block_stack(loader_ctx, cur_block, error_buf, @@ -12685,30 +13396,62 @@ wasm_loader_prepare_bytecode(WASMModule *module, WASMFunction *func, POP_CSP(); #if WASM_ENABLE_FAST_INTERP != 0 - skip_label(); - /* copy the result to the block return address */ - if (!reserve_block_ret(loader_ctx, opcode, disable_emit, - error_buf, error_buf_size)) { - /* it could be tmp frame_csp allocated from opcode like - * OP_BR and not counted in loader_ctx->csp_num, it won't - * be freed in wasm_loader_ctx_destroy(loader_ctx) so need - * to free the loader_ctx->frame_csp if fails */ +#if WASM_ENABLE_EXCE_HANDLING != 0 + if (ending_was_eh) { + /* try-region END must execute the eh-stack pop in + * the runtime END handler — including when reached + * via `br N` (whose target was registered into + * this block's PATCH_END list by emit_br_info). + * + * Rewind the auto-emitted END byte, point all + * PATCH_END entries at the rewound position, then + * re-emit the END byte so both branches and fall- + * through dispatch the pop. reserve_block_ret's + * COPY (if any) lands *after* the END byte: the + * pop only adjusts eh_count and doesn't touch the + * operand stack the COPY moves from. */ + skip_label(); + apply_label_patch(loader_ctx, 0, PATCH_END); + emit_label(WASM_OP_END); + if (!reserve_block_ret(loader_ctx, opcode, disable_emit, + error_buf, error_buf_size)) { + free_label_patch_list(loader_ctx->frame_csp); + goto fail; + } free_label_patch_list(loader_ctx->frame_csp); - goto fail; + /* A try-region's END can never coincide with + * LABEL_TYPE_FUNCTION (the implicit function block + * is not a try); no WASM_OP_RETURN emit needed. */ } + else +#endif /* WASM_ENABLE_EXCE_HANDLING */ + { + skip_label(); + /* copy the result to the block return address */ + if (!reserve_block_ret(loader_ctx, opcode, disable_emit, + error_buf, error_buf_size)) { + /* it could be tmp frame_csp allocated from opcode like + * OP_BR and not counted in loader_ctx->csp_num, it + * won't be freed in wasm_loader_ctx_destroy(loader_ctx) + * so need to free the loader_ctx->frame_csp if fails */ + free_label_patch_list(loader_ctx->frame_csp); + goto fail; + } - apply_label_patch(loader_ctx, 0, PATCH_END); - free_label_patch_list(loader_ctx->frame_csp); - if (loader_ctx->frame_csp->label_type == LABEL_TYPE_FUNCTION) { - int32 idx; - uint8 ret_type; - - emit_label(WASM_OP_RETURN); - for (idx = (int32)func->func_type->result_count - 1; - idx >= 0; idx--) { - ret_type = *(func->func_type->types - + func->func_type->param_count + idx); - POP_OFFSET_TYPE(ret_type); + apply_label_patch(loader_ctx, 0, PATCH_END); + free_label_patch_list(loader_ctx->frame_csp); + if (loader_ctx->frame_csp->label_type + == LABEL_TYPE_FUNCTION) { + int32 idx; + uint8 ret_type; + + emit_label(WASM_OP_RETURN); + for (idx = (int32)func->func_type->result_count - 1; + idx >= 0; idx--) { + ret_type = *(func->func_type->types + + func->func_type->param_count + idx); + POP_OFFSET_TYPE(ret_type); + } } } #endif @@ -12733,6 +13476,22 @@ wasm_loader_prepare_bytecode(WASMModule *module, WASMFunction *func, } #endif +#if WASM_ENABLE_FAST_INTERP != 0 && WASM_ENABLE_EXCE_HANDLING != 0 + /* Second-traverse-only: if this END closed a try- + * region, record where the rewritten IR continues so a + * runtime catch-handler body can branch past the + * region after running. The captured pc lands *after* + * the END's own skip_label and reserve_block_ret, so + * the next dispatched op is whatever follows the + * source-level END byte. */ + if (loader_ctx->p_code_compiled != NULL && ending_was_eh) { + bh_assert(ending_eh_idx < func->exception_handler_count); + bh_assert(func->exception_handlers != NULL); + func->exception_handlers[ending_eh_idx].end_of_region_pc = + loader_ctx->p_code_compiled; + } +#endif + break; } @@ -12743,6 +13502,47 @@ wasm_loader_prepare_bytecode(WASMModule *module, WASMFunction *func, error_buf, error_buf_size))) goto fail; +#if WASM_ENABLE_EXCE_HANDLING != 0 && WASM_ENABLE_FAST_INTERP != 0 + /* When a br skips over a try-region's END, the + * runtime br doesn't pop eh-stack entries. For a + * one-shot br to a block / function-end / catch, + * the leaked entry is absorbed by the static + * `exception_handler_count * EH_ENTRY_CELLS` + * reservation and dies at frame teardown — log + * a warning so the shape shows up in load-time + * diagnostics, but accept the module. + * + * If the br target is a LOOP entry, however, + * every iteration's TRY push adds one more entry + * to the eh-stack and eventually overwrites past + * the static reservation (silently in release + * builds since `bh_assert` is a no-op without + * `BH_DEBUG`). Reject those modules at load time + * — emitting cleanup at the br site would be the + * other fix, but it complicates the hot dispatch + * loop and the shape is rare in practice. */ + { + uint32 leaked = count_try_blocks_crossed( + loader_ctx->frame_csp - 1, frame_csp_tmp); + if (leaked > 0 + && br_try_leak_in_enclosing_loop( + loader_ctx->frame_csp_bottom, frame_csp_tmp)) { + set_error_buf(error_buf, error_buf_size, + "br out of try-region from inside a " + "loop not supported in fast " + "interpreter (would leak eh-stack " + "entries per iteration)"); + goto fail; + } + if (leaked > 0 && loader_ctx->p_code_compiled == NULL) { + LOG_WARNING("wasm fast-interp: br at func[%u] crosses " + "%u try-region(s); each leaks one " + "eh-stack entry until frame teardown", + cur_func_idx, leaked); + } + } +#endif + RESET_STACK(); SET_CUR_BLOCK_STACK_POLYMORPHIC_STATE(true); break; @@ -12757,6 +13557,30 @@ wasm_loader_prepare_bytecode(WASMModule *module, WASMFunction *func, error_buf, error_buf_size))) goto fail; +#if WASM_ENABLE_EXCE_HANDLING != 0 && WASM_ENABLE_FAST_INTERP != 0 + { + uint32 leaked = count_try_blocks_crossed( + loader_ctx->frame_csp - 1, frame_csp_tmp); + if (leaked > 0 + && br_try_leak_in_enclosing_loop( + loader_ctx->frame_csp_bottom, frame_csp_tmp)) { + set_error_buf(error_buf, error_buf_size, + "br_if out of try-region from inside a " + "loop not supported in fast " + "interpreter (would leak eh-stack " + "entries per iteration)"); + goto fail; + } + if (leaked > 0 && loader_ctx->p_code_compiled == NULL) { + LOG_WARNING( + "wasm fast-interp: br_if at func[%u] crosses " + "%u try-region(s); each leaks one " + "eh-stack entry until frame teardown", + cur_func_idx, leaked); + } + } +#endif + break; } @@ -12823,6 +13647,31 @@ wasm_loader_prepare_bytecode(WASMModule *module, WASMFunction *func, goto fail; } +#if WASM_ENABLE_EXCE_HANDLING != 0 && WASM_ENABLE_FAST_INTERP != 0 + { + uint32 leaked = count_try_blocks_crossed( + loader_ctx->frame_csp - 1, frame_csp_tmp); + if (leaked > 0 + && br_try_leak_in_enclosing_loop( + loader_ctx->frame_csp_bottom, frame_csp_tmp)) { + set_error_buf(error_buf, error_buf_size, + "br_table out of try-region from " + "inside a loop not supported in fast " + "interpreter (would leak eh-stack " + "entries per iteration)"); + goto fail; + } + if (leaked > 0 && loader_ctx->p_code_compiled == NULL) { + LOG_WARNING( + "wasm fast-interp: br_table[%u] at " + "func[%u] crosses %u try-region(s); each " + "leaks one eh-stack entry until frame " + "teardown", + i, cur_func_idx, leaked); + } + } +#endif + #if WASM_ENABLE_FAST_INTERP == 0 if (br_table_cache) { br_table_cache->br_depths[i] = depth; diff --git a/core/iwasm/interpreter/wasm_mini_loader.c b/core/iwasm/interpreter/wasm_mini_loader.c index 1e2aa08c62..84af801cb7 100644 --- a/core/iwasm/interpreter/wasm_mini_loader.c +++ b/core/iwasm/interpreter/wasm_mini_loader.c @@ -500,6 +500,13 @@ load_init_expr(WASMModule *module, const uint8 **p_buf, const uint8 *buf_end, CHECK_BUF(p, p_end, 1); type1 = read_uint8(p); + if (type1 != VALUE_TYPE_FUNCREF + && type1 != VALUE_TYPE_EXTERNREF) { + set_error_buf(error_buf, error_buf_size, + "invalid reference type"); + goto fail; + } + cur_value.ref_index = UINT32_MAX; if (!push_const_expr_stack(&const_expr_ctx, flag, type1, &cur_value, diff --git a/core/iwasm/libraries/debug-engine/debug_engine.h b/core/iwasm/libraries/debug-engine/debug_engine.h index 275eeaad13..891a0be477 100644 --- a/core/iwasm/libraries/debug-engine/debug_engine.h +++ b/core/iwasm/libraries/debug-engine/debug_engine.h @@ -109,8 +109,8 @@ typedef enum WasmAddressType { #define WASM_ADDR(type, id, offset) \ (((uint64)type << 62) | ((uint64)0 << 32) | ((uint64)offset << 0)) -#define WASM_ADDR_TYPE(addr) (((addr)&0xC000000000000000) >> 62) -#define WASM_ADDR_OFFSET(addr) (((addr)&0x00000000FFFFFFFF)) +#define WASM_ADDR_TYPE(addr) (((addr) & 0xC000000000000000) >> 62) +#define WASM_ADDR_OFFSET(addr) (((addr) & 0x00000000FFFFFFFF)) #define INVALIED_ADDR (0xFFFFFFFFFFFFFFFF) diff --git a/core/iwasm/libraries/libc-uvwasi/libc_uvwasi_wrapper.c b/core/iwasm/libraries/libc-uvwasi/libc_uvwasi_wrapper.c index 35d091e78d..fad77406fe 100644 --- a/core/iwasm/libraries/libc-uvwasi/libc_uvwasi_wrapper.c +++ b/core/iwasm/libraries/libc-uvwasi/libc_uvwasi_wrapper.c @@ -927,13 +927,16 @@ wasi_poll_oneoff(wasm_exec_env_t exec_env, const wasi_subscription_t *in, wasm_module_inst_t module_inst = get_module_inst(exec_env); uvwasi_t *uvwasi = get_wasi_ctx(module_inst); uvwasi_size_t nevents; + uint64 subscriptions_size = + (uint64)nsubscriptions * sizeof(wasi_subscription_t); + uint64 events_size = (uint64)nsubscriptions * sizeof(wasi_event_t); wasi_errno_t err; if (!uvwasi) return (wasi_errno_t)-1; - if (!validate_native_addr((void *)in, (uint64)sizeof(wasi_subscription_t)) - || !validate_native_addr(out, (uint64)sizeof(wasi_event_t)) + if (!validate_native_addr((void *)in, subscriptions_size) + || !validate_native_addr(out, events_size) || !validate_native_addr(nevents_app, (uint64)sizeof(uint32))) return (wasi_errno_t)-1; diff --git a/core/iwasm/libraries/libc-wasi/libc_wasi_wrapper.c b/core/iwasm/libraries/libc-wasi/libc_wasi_wrapper.c index 5ab189e71d..7eebf12ef9 100644 --- a/core/iwasm/libraries/libc-wasi/libc_wasi_wrapper.c +++ b/core/iwasm/libraries/libc-wasi/libc_wasi_wrapper.c @@ -1093,13 +1093,16 @@ wasi_poll_oneoff(wasm_exec_env_t exec_env, const wasi_subscription_t *in, wasi_ctx_t wasi_ctx = get_wasi_ctx(module_inst); struct fd_table *curfds = wasi_ctx_get_curfds(wasi_ctx); size_t nevents = 0; + uint64 subscriptions_size = + (uint64)nsubscriptions * sizeof(wasi_subscription_t); + uint64 events_size = (uint64)nsubscriptions * sizeof(wasi_event_t); wasi_errno_t err; if (!wasi_ctx) return (wasi_errno_t)-1; - if (!validate_native_addr((void *)in, (uint64)sizeof(wasi_subscription_t)) - || !validate_native_addr(out, (uint64)sizeof(wasi_event_t)) + if (!validate_native_addr((void *)in, subscriptions_size) + || !validate_native_addr(out, events_size) || !validate_native_addr(nevents_app, (uint64)sizeof(uint32))) return (wasi_errno_t)-1; diff --git a/core/shared/mem-alloc/ems/ems_gc_internal.h b/core/shared/mem-alloc/ems/ems_gc_internal.h index 417364bab4..01ab254e3b 100644 --- a/core/shared/mem-alloc/ems/ems_gc_internal.h +++ b/core/shared/mem-alloc/ems/ems_gc_internal.h @@ -77,7 +77,7 @@ hmu_verify(void *vheap, hmu_t *hmu); #endif /* end of BH_ENABLE_GC_VERIFY */ -#define hmu_obj_size(s) ((s)-OBJ_EXTRA_SIZE) +#define hmu_obj_size(s) ((s) - OBJ_EXTRA_SIZE) #define GC_ALIGN_8(s) (((uint32)(s) + 7) & (uint32)~7) @@ -201,11 +201,11 @@ hmu_verify(void *vheap, hmu_t *hmu); /* Get magic pointer from aligned object pointer */ #define ALIGNED_ALLOC_GET_MAGIC_PTR(obj) \ - ((uint32_t *)((char *)(obj)-ALIGNED_ALLOC_MAGIC_SIZE)) + ((uint32_t *)((char *)(obj) - ALIGNED_ALLOC_MAGIC_SIZE)) /* Get offset pointer from aligned object pointer */ #define ALIGNED_ALLOC_GET_OFFSET_PTR(obj) \ - ((uint32_t *)((char *)(obj)-ALIGNED_ALLOC_METADATA_SIZE)) + ((uint32_t *)((char *)(obj) - ALIGNED_ALLOC_METADATA_SIZE)) /* Extra overhead for aligned allocations beyond normal OBJ_EXTRA_SIZE */ #define ALIGNED_ALLOC_EXTRA_OVERHEAD ALIGNED_ALLOC_METADATA_SIZE diff --git a/core/shared/platform/alios/alios_platform.c b/core/shared/platform/alios/alios_platform.c index a3752b4395..e37edce825 100644 --- a/core/shared/platform/alios/alios_platform.c +++ b/core/shared/platform/alios/alios_platform.c @@ -38,7 +38,8 @@ os_realloc(void *ptr, unsigned size) void os_free(void *ptr) -{} +{ +} int os_dumps_proc_mem_info(char *out, unsigned int size) @@ -74,11 +75,13 @@ os_mprotect(void *addr, size_t size, int prot) void os_dcache_flush() -{} +{ +} void os_icache_flush(void *start, size_t len) -{} +{ +} os_raw_file_handle os_invalid_raw_handle(void) diff --git a/core/shared/platform/alios/alios_thread.c b/core/shared/platform/alios/alios_thread.c index 9fe927db0e..ba3050d345 100644 --- a/core/shared/platform/alios/alios_thread.c +++ b/core/shared/platform/alios/alios_thread.c @@ -362,4 +362,5 @@ os_thread_get_stack_boundary() void os_thread_jit_write_protect_np(bool enabled) -{} \ No newline at end of file +{ +} \ No newline at end of file diff --git a/core/shared/platform/android/platform_init.c b/core/shared/platform/android/platform_init.c index ad206af0ee..4e52e1e464 100644 --- a/core/shared/platform/android/platform_init.c +++ b/core/shared/platform/android/platform_init.c @@ -19,7 +19,8 @@ bh_platform_init() void bh_platform_destroy() -{} +{ +} int os_printf(const char *fmt, ...) diff --git a/core/shared/platform/common/freertos/freertos_malloc.c b/core/shared/platform/common/freertos/freertos_malloc.c index e47a8cce16..f9d5b0ec7c 100644 --- a/core/shared/platform/common/freertos/freertos_malloc.c +++ b/core/shared/platform/common/freertos/freertos_malloc.c @@ -19,7 +19,8 @@ os_realloc(void *ptr, unsigned size) void os_free(void *ptr) -{} +{ +} int os_dumps_proc_mem_info(char *out, unsigned int size) diff --git a/core/shared/platform/common/math/math.c b/core/shared/platform/common/math/math.c index 3c0171570f..e8d186a0d2 100644 --- a/core/shared/platform/common/math/math.c +++ b/core/shared/platform/common/math/math.c @@ -1408,7 +1408,8 @@ freebsd_pow(double x, double y) n = (hx>>31)+1; but ANSI C says a right shift of a signed negative quantity is implementation defined. */ - n = ((u_int32_t)hx >> 31) - 1; + /* equal to n = (hx < 0) - 1. But if it works, don't improve it */ + n = (int32_t)((u_int32_t)hx >> 31) - 1; /* (x<0)**(non-int) is NaN */ if ((n | yisint) == 0) diff --git a/core/shared/platform/common/posix/posix_memmap.c b/core/shared/platform/common/posix/posix_memmap.c index d5cad885ca..c29533660e 100644 --- a/core/shared/platform/common/posix/posix_memmap.c +++ b/core/shared/platform/common/posix/posix_memmap.c @@ -288,7 +288,8 @@ os_mprotect(void *addr, size_t size, int prot) void os_dcache_flush(void) -{} +{ +} void os_icache_flush(void *start, size_t len) diff --git a/core/shared/platform/cosmopolitan/platform_init.c b/core/shared/platform/cosmopolitan/platform_init.c index 2aae13fa14..2856fc9ca8 100644 --- a/core/shared/platform/cosmopolitan/platform_init.c +++ b/core/shared/platform/cosmopolitan/platform_init.c @@ -13,7 +13,8 @@ bh_platform_init() void bh_platform_destroy() -{} +{ +} int os_printf(const char *format, ...) diff --git a/core/shared/platform/darwin/platform_init.c b/core/shared/platform/darwin/platform_init.c index 2aae13fa14..2856fc9ca8 100644 --- a/core/shared/platform/darwin/platform_init.c +++ b/core/shared/platform/darwin/platform_init.c @@ -13,7 +13,8 @@ bh_platform_init() void bh_platform_destroy() -{} +{ +} int os_printf(const char *format, ...) diff --git a/core/shared/platform/esp-idf/espidf_memmap.c b/core/shared/platform/esp-idf/espidf_memmap.c index b0c493d2de..23b23d13d9 100644 --- a/core/shared/platform/esp-idf/espidf_memmap.c +++ b/core/shared/platform/esp-idf/espidf_memmap.c @@ -127,7 +127,8 @@ void void os_icache_flush(void *start, size_t len) -{} +{ +} #if (WASM_MEM_DUAL_BUS_MIRROR != 0) void * diff --git a/core/shared/platform/esp-idf/espidf_platform.c b/core/shared/platform/esp-idf/espidf_platform.c index 045c3a5f6d..a9b3d4df39 100644 --- a/core/shared/platform/esp-idf/espidf_platform.c +++ b/core/shared/platform/esp-idf/espidf_platform.c @@ -30,7 +30,8 @@ bh_platform_init() void bh_platform_destroy() -{} +{ +} int os_printf(const char *format, ...) @@ -86,7 +87,8 @@ os_thread_get_stack_boundary(void) void os_thread_jit_write_protect_np(bool enabled) -{} +{ +} int os_usleep(uint32 usec) diff --git a/core/shared/platform/freebsd/platform_init.c b/core/shared/platform/freebsd/platform_init.c index 2aae13fa14..2856fc9ca8 100644 --- a/core/shared/platform/freebsd/platform_init.c +++ b/core/shared/platform/freebsd/platform_init.c @@ -13,7 +13,8 @@ bh_platform_init() void bh_platform_destroy() -{} +{ +} int os_printf(const char *format, ...) diff --git a/core/shared/platform/include/platform_common.h b/core/shared/platform/include/platform_common.h index 28001af746..ea610e0cb0 100644 --- a/core/shared/platform/include/platform_common.h +++ b/core/shared/platform/include/platform_common.h @@ -19,11 +19,11 @@ extern "C" { #define BHT_TIMED_OUT (1) #define BHT_OK (0) -#define BHT_WAIT_FOREVER ((uint64)-1LL) +#define BHT_WAIT_FOREVER ((uint64) - 1LL) #define BH_KB (1024) -#define BH_MB ((BH_KB)*1024) -#define BH_GB ((BH_MB)*1024) +#define BH_MB ((BH_KB) * 1024) +#define BH_GB ((BH_MB) * 1024) #ifndef BH_MALLOC #define BH_MALLOC os_malloc @@ -39,11 +39,15 @@ extern "C" { #if defined(_MSC_BUILD) #if defined(COMPILING_WASM_RUNTIME_API) -__declspec(dllexport) void *BH_MALLOC(unsigned int size); -__declspec(dllexport) void BH_FREE(void *ptr); +__declspec(dllexport) void * +BH_MALLOC(unsigned int size); +__declspec(dllexport) void +BH_FREE(void *ptr); #else -__declspec(dllimport) void *BH_MALLOC(unsigned int size); -__declspec(dllimport) void BH_FREE(void *ptr); +__declspec(dllimport) void * +BH_MALLOC(unsigned int size); +__declspec(dllimport) void +BH_FREE(void *ptr); #endif #else void * @@ -54,7 +58,8 @@ BH_FREE(void *ptr); #if defined(BH_VPRINTF) #if defined(MSVC) -__declspec(dllimport) int BH_VPRINTF(const char *format, va_list ap); +__declspec(dllimport) int +BH_VPRINTF(const char *format, va_list ap); #else int BH_VPRINTF(const char *format, va_list ap); diff --git a/core/shared/platform/linux-sgx/sgx_file.h b/core/shared/platform/linux-sgx/sgx_file.h index 8690e1f69c..3bc9cdaf4e 100644 --- a/core/shared/platform/linux-sgx/sgx_file.h +++ b/core/shared/platform/linux-sgx/sgx_file.h @@ -63,13 +63,13 @@ extern "C" { #define SEEK_CUR 1 #define SEEK_END 2 -#define S_ISDIR(mode) (((mode)&S_IFMT) == S_IFDIR) -#define S_ISCHR(mode) (((mode)&S_IFMT) == S_IFCHR) -#define S_ISBLK(mode) (((mode)&S_IFMT) == S_IFBLK) -#define S_ISREG(mode) (((mode)&S_IFMT) == S_IFREG) -#define S_ISFIFO(mode) (((mode)&S_IFMT) == S_IFIFO) -#define S_ISLNK(mode) (((mode)&S_IFMT) == S_IFLNK) -#define S_ISSOCK(mode) (((mode)&S_IFMT) == S_IFSOCK) +#define S_ISDIR(mode) (((mode) & S_IFMT) == S_IFDIR) +#define S_ISCHR(mode) (((mode) & S_IFMT) == S_IFCHR) +#define S_ISBLK(mode) (((mode) & S_IFMT) == S_IFBLK) +#define S_ISREG(mode) (((mode) & S_IFMT) == S_IFREG) +#define S_ISFIFO(mode) (((mode) & S_IFMT) == S_IFIFO) +#define S_ISLNK(mode) (((mode) & S_IFMT) == S_IFLNK) +#define S_ISSOCK(mode) (((mode) & S_IFMT) == S_IFSOCK) #define DT_UNKNOWN 0 #define DT_FIFO 1 diff --git a/core/shared/platform/linux-sgx/sgx_thread.c b/core/shared/platform/linux-sgx/sgx_thread.c index 73f3005fab..a577564ef8 100644 --- a/core/shared/platform/linux-sgx/sgx_thread.c +++ b/core/shared/platform/linux-sgx/sgx_thread.c @@ -213,7 +213,8 @@ os_thread_get_stack_boundary() void os_thread_jit_write_protect_np(bool enabled) -{} +{ +} int os_rwlock_init(korp_rwlock *lock) diff --git a/core/shared/platform/linux/platform_init.c b/core/shared/platform/linux/platform_init.c index 2aae13fa14..2856fc9ca8 100644 --- a/core/shared/platform/linux/platform_init.c +++ b/core/shared/platform/linux/platform_init.c @@ -13,7 +13,8 @@ bh_platform_init() void bh_platform_destroy() -{} +{ +} int os_printf(const char *format, ...) diff --git a/core/shared/platform/nuttx/nuttx_platform.c b/core/shared/platform/nuttx/nuttx_platform.c index da5bf86736..8780da7059 100644 --- a/core/shared/platform/nuttx/nuttx_platform.c +++ b/core/shared/platform/nuttx/nuttx_platform.c @@ -20,7 +20,8 @@ bh_platform_init() void bh_platform_destroy() -{} +{ +} void * os_malloc(unsigned size) @@ -287,7 +288,8 @@ getaddrinfo(FAR const char *nodename, FAR const char *servname, void freeaddrinfo(FAR struct addrinfo *ai) -{} +{ +} int setsockopt(int sockfd, int level, int option, FAR const void *value, diff --git a/core/shared/platform/riot/riot_platform.c b/core/shared/platform/riot/riot_platform.c index b48033247a..ec1fa4c6fb 100644 --- a/core/shared/platform/riot/riot_platform.c +++ b/core/shared/platform/riot/riot_platform.c @@ -94,7 +94,8 @@ os_dcache_flush(void) void os_icache_flush(void *start, size_t len) -{} +{ +} os_raw_file_handle os_invalid_raw_handle(void) diff --git a/core/shared/platform/riot/riot_thread.c b/core/shared/platform/riot/riot_thread.c index 893ed0b456..059266612c 100644 --- a/core/shared/platform/riot/riot_thread.c +++ b/core/shared/platform/riot/riot_thread.c @@ -433,4 +433,5 @@ os_thread_get_stack_boundary() void os_thread_jit_write_protect_np(bool enabled) -{} \ No newline at end of file +{ +} \ No newline at end of file diff --git a/core/shared/platform/rt-thread/rtt_platform.c b/core/shared/platform/rt-thread/rtt_platform.c index 904bb52ed1..125bd9451c 100644 --- a/core/shared/platform/rt-thread/rtt_platform.c +++ b/core/shared/platform/rt-thread/rtt_platform.c @@ -21,7 +21,8 @@ bh_platform_init(void) void bh_platform_destroy(void) -{} +{ +} void * os_malloc(unsigned size) @@ -179,11 +180,13 @@ os_mprotect(void *addr, size_t size, int prot) void os_dcache_flush(void) -{} +{ +} void os_icache_flush(void *start, size_t len) -{} +{ +} int os_getpagesize(void) diff --git a/core/shared/platform/rt-thread/rtt_thread.c b/core/shared/platform/rt-thread/rtt_thread.c index 5f988fad0d..ec4808d7b7 100644 --- a/core/shared/platform/rt-thread/rtt_thread.c +++ b/core/shared/platform/rt-thread/rtt_thread.c @@ -186,7 +186,8 @@ os_thread_get_stack_boundary(void) void os_thread_jit_write_protect_np(bool enabled) -{} +{ +} int os_mutex_init(korp_mutex *mutex) diff --git a/core/shared/platform/vxworks/platform_init.c b/core/shared/platform/vxworks/platform_init.c index 2aae13fa14..2856fc9ca8 100644 --- a/core/shared/platform/vxworks/platform_init.c +++ b/core/shared/platform/vxworks/platform_init.c @@ -13,7 +13,8 @@ bh_platform_init() void bh_platform_destroy() -{} +{ +} int os_printf(const char *format, ...) diff --git a/core/shared/platform/windows/platform_init.c b/core/shared/platform/windows/platform_init.c index 96bcf9ab1a..e6db03064f 100644 --- a/core/shared/platform/windows/platform_init.c +++ b/core/shared/platform/windows/platform_init.c @@ -72,8 +72,10 @@ os_getpagesize() void os_dcache_flush(void) -{} +{ +} void os_icache_flush(void *start, size_t len) -{} \ No newline at end of file +{ +} \ No newline at end of file diff --git a/core/shared/platform/windows/win_file.c b/core/shared/platform/windows/win_file.c index 7cfda4cc3a..878d6aa17a 100644 --- a/core/shared/platform/windows/win_file.c +++ b/core/shared/platform/windows/win_file.c @@ -7,7 +7,7 @@ #include "libc_errno.h" #include "win_util.h" -#include "PathCch.h" +#include "pathcch.h" #pragma comment(lib, "Pathcch.lib") @@ -1295,26 +1295,25 @@ os_readlinkat(os_file_handle handle, const char *path, char *buf, if (wbufsize >= 4 && wbuf[0] == L'\\' && wbuf[1] == L'?' && wbuf[2] == L'?' && wbuf[3] == L'\\') { - // Starts with \??\ + // Starts with \??\ prefix if (wbufsize >= 6 && ((wbuf[4] >= L'A' && wbuf[4] <= L'Z') || (wbuf[4] >= L'a' && wbuf[4] <= L'z')) - && wbuf[5] == L':' && (wbufsize == 6 || wbuf[6] == L'\\')) - { - // \??\:\ - wbuf += 4; - wbufsize -= 4; - } - else if (wbufsize >= 8 && (wbuf[4] == L'U' || wbuf[4] == L'u') - && (wbuf[5] == L'N' || wbuf[5] == L'n') - && (wbuf[6] == L'C' || wbuf[6] == L'c') - && wbuf[7] == L'\\') - { - // \??\UNC\\\ - make sure the final path looks like \\\\ - wbuf += 6; - wbuf[0] = L'\\'; - wbufsize -= 6; - } + && wbuf[5] == L':' && (wbufsize == 6 || wbuf[6] == L'\\')) { + // \??\:\ form + wbuf += 4; + wbufsize -= 4; + } + else if (wbufsize >= 8 && (wbuf[4] == L'U' || wbuf[4] == L'u') + && (wbuf[5] == L'N' || wbuf[5] == L'n') + && (wbuf[6] == L'C' || wbuf[6] == L'c') + && wbuf[7] == L'\\') { + // \??\UNC\\\ - make sure the final path looks + // like \\\\ afterwards + wbuf += 6; + wbuf[0] = L'\\'; + wbufsize -= 6; + } } } else if (reparse_data->ReparseTag == IO_REPARSE_TAG_MOUNT_POINT) { diff --git a/core/shared/platform/windows/win_thread.c b/core/shared/platform/windows/win_thread.c index 1f6a57ebbf..fb6353b6ad 100644 --- a/core/shared/platform/windows/win_thread.c +++ b/core/shared/platform/windows/win_thread.c @@ -242,7 +242,8 @@ os_thread_cleanup(void *retval) os_mutex_unlock(&thread_data->wait_lock); } -static unsigned __stdcall os_thread_wrapper(void *arg) +static unsigned __stdcall +os_thread_wrapper(void *arg) { os_thread_data *thread_data = arg; os_thread_data *parent = thread_data->parent; @@ -830,7 +831,8 @@ os_thread_get_stack_boundary() void os_thread_jit_write_protect_np(bool enabled) -{} +{ +} #ifdef OS_ENABLE_HW_BOUND_CHECK static os_thread_local_attribute bool thread_signal_inited = false; diff --git a/core/shared/platform/zephyr/zephyr_thread.c b/core/shared/platform/zephyr/zephyr_thread.c index af864e417e..9f8d491e09 100644 --- a/core/shared/platform/zephyr/zephyr_thread.c +++ b/core/shared/platform/zephyr/zephyr_thread.c @@ -586,7 +586,8 @@ os_thread_get_stack_boundary() void os_thread_jit_write_protect_np(bool enabled) -{} +{ +} int os_rwlock_init(korp_rwlock *lock) diff --git a/doc/security_issue_runbook.md b/doc/security_issue_runbook.md index 8b0bc7970b..54cc31f25a 100644 --- a/doc/security_issue_runbook.md +++ b/doc/security_issue_runbook.md @@ -17,6 +17,7 @@ For information on what types of issues are considered security vulnerabilities ## Step 3: Communication and Collaboration - Use Non-Public Channels: Communicate through non-public channels, preferably email, during the resolution process. Avoid filing issues or pull requests on third-party repositories if they are involved. +- Work should be done in a private fork created for the security advisory. Additional collaborators should be invited to this private fork as needed. - Workaround for Third-Party Dependencies: If third-party dependencies are involved, consider a workaround to patch the issue quickly unless the third party can release a fix promptly. ## Step 4: Finalizing and Preparing for Release @@ -28,7 +29,7 @@ For information on what types of issues are considered security vulnerabilities ``` markdown > A template for the advanced disclosure email -The Wamr project would like to announce a forthcoming security release. +The WAMR project would like to announce a forthcoming security release. The release will be made available on approximately YYYY-MM-DD. Additionally, an advisory will be made available on the same date at https://github.com/advisories. @@ -41,9 +42,9 @@ The highest severity issue fixed in this release is classified as XXX based on t - Run Full Test Suite: Run the full test suite locally for the main branch. Attempt to run as much of the CI matrix locally as possible. ## Step 6: Public Release and Communication - +On Release day: - Open Version Bump PRs: Open version bump pull requests on the public repository without including patch notes or release notes for the fix. -- Manually Make PRs from Private Fork: Transfer the necessary pull requests from the private fork to the public repository. +- Manually Make PRs from Private Fork: Make public PRs from all of the previously-created PRs on the private fork. You'll need to push the changes to your own personal repository for this (in order to submit PRs to the official repository). It's ok since it's release day and time to make these fixes public anyway. NOTE: DO NOT merge via the GitHub Security Advisory as this has generally not worked well. - Merge and Trigger Releases: Merge the version bump PRs and trigger the release process. - Publish GitHub Advisories: Delete the private forks and use the Big Green Button to publish the advisory. - Send Security Release Email: Send a follow-up email to sec-announce@bytecodealliance.org describing the security release. Other communication channels can also be used to inform users about the security release. diff --git a/samples/debug-tools-optimized/CMakeLists.txt b/samples/debug-tools-optimized/CMakeLists.txt new file mode 100644 index 0000000000..622b96f981 --- /dev/null +++ b/samples/debug-tools-optimized/CMakeLists.txt @@ -0,0 +1,89 @@ +# Copyright (C) 2026 Intel Corporation. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +cmake_minimum_required(VERSION 3.14) + +include(CheckPIESupported) + +project(debug_tools_optimized_sample) + +list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/../cmake) + +# WAMR features switch +string(TOLOWER ${CMAKE_HOST_SYSTEM_NAME} WAMR_BUILD_PLATFORM) +if (APPLE) + add_definitions(-DBH_PLATFORM_DARWIN) +endif () + +set(CMAKE_SHARED_LIBRARY_LINK_C_FLAGS "") +set(CMAKE_SHARED_LIBRARY_LINK_CXX_FLAGS "") + +if (NOT DEFINED WAMR_BUILD_TARGET) + if (CMAKE_SYSTEM_PROCESSOR MATCHES "^(arm64|aarch64)") + set (WAMR_BUILD_TARGET "AARCH64") + elseif (CMAKE_SYSTEM_PROCESSOR STREQUAL "riscv64") + set (WAMR_BUILD_TARGET "RISCV64") + elseif (CMAKE_SIZEOF_VOID_P EQUAL 8) + set (WAMR_BUILD_TARGET "X86_64") + elseif (CMAKE_SIZEOF_VOID_P EQUAL 4) + set (WAMR_BUILD_TARGET "X86_32") + else () + message(SEND_ERROR "Unsupported build target platform!") + endif () +endif () + +if (NOT CMAKE_BUILD_TYPE) + set (CMAKE_BUILD_TYPE Release) +endif () + +option(USE_FAST_INTERP "Build iwasm with fast interpreter instead of classic interpreter" OFF) + +set(WAMR_BUILD_INTERP 1) +set(WAMR_BUILD_LIBC_WASI 1) +if (USE_FAST_INTERP) + set(WAMR_BUILD_FAST_INTERP 1) + message(STATUS "Building iwasm with fast interpreter (USE_FAST_INTERP=ON)") +else () + set(WAMR_BUILD_FAST_INTERP 0) + message(STATUS "Building iwasm with classic interpreter (default; pass -DUSE_FAST_INTERP=ON for fast-interp)") +endif () +set(WAMR_BUILD_AOT 1) +set(WAMR_BUILD_DUMP_CALL_STACK 1) +# Disable hardware bound checks so OOB memory traps go through the +# interpreter's exception path (which calls SYNC_ALL_TO_FRAME and captures +# frame_ip), instead of the SIGSEGV signal-handler-and-longjmp path +# (which doesn't update frame->ip). Capturing frame_ip is what lets +# addr2line.py recover inline frames for the OOB sample. +set(WAMR_DISABLE_HW_BOUND_CHECK 1) + +if (NOT (CMAKE_C_COMPILER MATCHES ".*clang.*" OR CMAKE_C_COMPILER_ID MATCHES ".*Clang")) + set (CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--gc-sections") +endif () + +set(WAMR_ROOT_DIR ${CMAKE_CURRENT_LIST_DIR}/../..) +include (${WAMR_ROOT_DIR}/build-scripts/runtime_lib.cmake) + +add_library(vmlib ${WAMR_RUNTIME_LIB_SOURCE}) + +################ wasm application ################ +include(ExternalProject) + +ExternalProject_Add(wasm-apps + SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/wasm-apps" + CONFIGURE_COMMAND ${CMAKE_COMMAND} -S ${CMAKE_CURRENT_SOURCE_DIR}/wasm-apps -B build + BUILD_COMMAND ${CMAKE_COMMAND} --build build + INSTALL_COMMAND ${CMAKE_COMMAND} --install build --prefix ${CMAKE_CURRENT_BINARY_DIR} +) + +################ wamr runtime ################ +include (${SHARED_DIR}/utils/uncommon/shared_uncommon.cmake) + +set (RUNTIME_SOURCE_ALL + ${CMAKE_CURRENT_LIST_DIR}/../../product-mini/platforms/linux/main.c + ${UNCOMMON_SHARED_SOURCE} +) +add_executable (iwasm ${RUNTIME_SOURCE_ALL}) +check_pie_supported() +set_target_properties (iwasm PROPERTIES POSITION_INDEPENDENT_CODE ON) +target_link_libraries(iwasm vmlib -lm -ldl) +add_dependencies(iwasm wasm-apps) diff --git a/samples/debug-tools-optimized/README.md b/samples/debug-tools-optimized/README.md new file mode 100644 index 0000000000..ac9c2814e9 --- /dev/null +++ b/samples/debug-tools-optimized/README.md @@ -0,0 +1,283 @@ +# debug-tools-optimized — Debugging Production-Optimized WASM + +This sample demonstrates symbolication of crashes in **production-optimized** WASM +binaries using the merged `addr2line.py`. The wasm apps are compiled with +`-Oz -g -flto` and post-processed with `wasm-opt -Oz -g`, then stripped to produce +minimal production binaries. A "debug companion" binary built in parallel retains +DWARF inline info, enabling source-level call stack recovery offline. + +## Why this exists alongside `samples/debug-tools/` + +The existing `samples/debug-tools/` sample uses `-O0 -g`: each function is preserved, +no inlining happens. This sample uses `-Oz -g -flto`, which: + +- Aggressively inlines functions across translation units (cross-TU inlining via LTO) +- Strips the production binary to minimum size +- Tests `addr2line.py`'s inline expansion (`DW_TAG_inlined_subroutine` resolution) + +If you only need to debug development builds, `samples/debug-tools/` is sufficient. +If you need to debug **shipped** binaries, this sample shows how. + +## Build pipeline + +``` +clang -Oz -g -flto source1.c source2.c → .wasm (intermediate) + └─ wasm-opt -Oz -g → .debug.wasm (companion: code + DWARF + names) + └─ llvm-strip --strip-all → .prod.wasm (production: code only) +``` + +## Why production is derived from the debug companion + +`wasm-opt -Oz` (without `-g`) and `wasm-opt -Oz -g` produce **structurally different +binaries**: `-g` inhibits some inlining passes to preserve DWARF integrity. If we ran +them as separate pipelines, the production binary's code offsets would not match the +companion's DWARF address space — offline decode would silently break. + +Instead, we run `wasm-opt -Oz -g` once and derive production by stripping the +companion (`llvm-strip --strip-all`). Custom sections (DWARF, names) live *after* the +code section in the WASM binary format, so stripping them doesn't shift code offsets. +This **guarantees byte-identical code** between production and companion. + +## Why `-flto` + +Without LTO, functions in separate `.c` files remain separate WASM functions even +under `-Oz`. With LTO, the compiler sees all sources as one unit and inlines +aggressively across files. This is what makes the `do_bad_access → trigger_oob → +app_main` chain collapse into a single WASM function with multiple inlined +subroutines. + +## Why `recurse()` is non-tail-recursive + +`stackoverflow_recurse.c` uses +`int r = recurse(depth + 1); return r + buf[0];` instead of +`return recurse(depth + 1);`. Tail calls (`return f(...)`) get converted to loops at +`-Oz -flto`, eliminating the recursion that we want to test. The non-tail form forces +a real `call` instruction so each iteration pushes a new frame. + +## Prerequisites + +- wasi-sdk at `WASI_SDK_PATH` or `/opt/wasi-sdk` +- binaryen at `BINARYEN_PATH` or `/opt/binaryen` +- wabt at `WABT_PATH` or `/opt/wabt` +- Python 3 + +## Quick start + +```bash +mkdir -p build && cd build +cmake .. && make -j$(nproc) +cd .. + +# wasm (interpreter) +./symbolicate.sh oob +./symbolicate.sh stackoverflow + +# aot +./symbolicate.sh oob aot +./symbolicate.sh stackoverflow aot +``` + +To build with the fast interpreter instead of the classic interpreter: + +```bash +mkdir -p build && cd build +cmake .. -DUSE_FAST_INTERP=ON && make -j$(nproc) +``` + +The same `symbolicate.sh` invocations work for both build modes — it auto-detects +which interpreter the iwasm binary uses (by inspecting it for the `wasm_interp_fast.c` +symbol) and passes the right `--mode` to `addr2line.py`. + +### `verify.sh` — assertion-based smoke test + +`verify.sh ` runs `symbolicate.sh` and asserts the +symbolicated output matches the expected shape: for the OOB sample, +all three inline frames (`do_bad_access (inlined into trigger_oob)`, +`trigger_oob (inlined into app_main)`, `app_main`) plus their source +files; for the stackoverflow sample, `recurse` and `app_main` resolved +across the recursive chain. It auto-detects classic vs fast-interp +builds and relaxes the OOB assertion for fast-interp + wasm (where the +runtime offset doesn't map to source, so addr2line.py only emits the +outermost function name). Used by CI; useful locally to check a build: + +```bash +./verify.sh oob wasm # PASS or fails with the captured output +./verify.sh stackoverflow aot +``` + +The build pipeline produces three artifacts per app: + +| Artifact | Purpose | +|----------|---------| +| `.debug.wasm` | Debug companion — DWARF + name section, used for symbolication | +| `.prod.wasm` | Production wasm — fully stripped, runs in interpreter mode | +| `.prod.aot` | Production AOT — wamrc-compiled with `--enable-dump-call-stack` | + +Both `.prod.wasm` and `.prod.aot` use the same `.debug.wasm` companion for offline +symbolication. AOT and classic-interp produce nearly identical call stack offsets +(within 1-2 bytes), and `addr2line.py` resolves both correctly. + +## Build-side tools (and why each is needed) + +This sample chains four compile-time and post-build tools, each filling a +specific gap; dropping any of them would break the inline-DWARF round trip +the sample is meant to demonstrate: + +| Tool | From | What it does here | Why we need it | +|------|------|-------------------|----------------| +| `clang -Oz -g -flto` | wasi-sdk | Compiles `.c` → `.wasm` with size-optimized code, DWARF debug info, and link-time optimization | `-flto` enables cross-translation-unit inlining. Without LTO, `oob_main.c` and `oob_access.c` would stay as separate WASM functions; with LTO they collapse into a single function with `DW_TAG_inlined_subroutine` entries — exactly what we need to test inline-aware symbolication. | +| `wasm-opt -Oz -g` | binaryen | Post-optimization pass on the wasm; preserves DWARF integrity | Further size shrink (LEB compaction, dead-code elimination) on top of clang. The `-g` flag inhibits transformations that would corrupt DWARF, producing a "debug companion" with the same code layout as production. | +| `llvm-strip --strip-all` | wasi-sdk | Strips DWARF and name sections from the companion → production binary | Custom sections (DWARF, name) live AFTER the code section in the wasm format, so stripping them doesn't shift code offsets. This guarantees the production binary's runtime offsets map 1:1 to the companion's DWARF — without this property, offline decode would silently break. | +| `wamrc --enable-dump-call-stack --bounds-checks=1` | wamr-compiler | Compiles `.wasm` → `.aot` for ahead-of-time execution | AOT is the realistic embedded deployment target. `--bounds-checks=1` forces software memory bounds checks instead of hardware traps, so OOB exceptions go through the runtime exception path (which captures `frame_ip`) instead of a SIGSEGV that bypasses ip capture — without this, the OOB sample's AOT path would crash with no captured call stack. | + +For the **decode-side** tools (`llvm-symbolizer`, `llvm-dwarfdump`, +`wasm-objdump`, `llvm-cxxfilt`) and the interval-table overlay that +addr2line.py applies to correct the LLVM symbolizer's outermost +function-name lookup on wasm, see +[`test-tools/addr2line/README.md`](../../test-tools/addr2line/README.md). + +## Three execution modes, three offset spaces + +`addr2line.py` supports a `--mode={interp,aot,fast-interp}` flag because each +mode reports offsets differently: + +| Mode | Offset space | Adjustment | Source resolution | +|------|--------------|-----------|-------------------| +| `interp` (default) | File-absolute, post-advance | `offset - code_start - 1` | Full file:line, inline expansion | +| `aot` | File-absolute, instruction-start | `offset - code_start` | Full file:line, inline expansion | +| `fast-interp` | Function-relative, transformed bytecode | (not mappable) | Function-name only | + +**Why fast-interp can't show source lines**: at load time, fast-interp rewrites the +WASM bytecode in memory (replaces opcodes with handler indices, expands LEBs to +fixed widths, etc.). The runtime ip then points into this *transformed* buffer, not +the original WASM bytes — so there's no way to map the offset back to a source line. +Function-name lookup still works via `wasm-objdump` + `llvm-dwarfdump --name=...`. + +`symbolicate.sh` handles all three modes transparently. For manual invocation: + +```bash +# Classic interp (default) +python3 ../../test-tools/addr2line/addr2line.py --wasm-file ... callstack.txt + +# AOT +python3 ../../test-tools/addr2line/addr2line.py --mode=aot --wasm-file ... callstack.txt + +# Fast interp +python3 ../../test-tools/addr2line/addr2line.py --mode=fast-interp --wasm-file ... callstack.txt +``` + +## Why `iwasm -f app_main` (and not just `iwasm `) + +The `symbolicate.sh` script invokes `iwasm -f app_main` instead of letting iwasm run +the default wasi `_start` entry. This matters for two reasons: + +1. **Compiler folding**: Under `-Oz -flto`, when control reaches the OOB write through + `_start → __wasi_main_void → main → app_main → ...`, LLVM observes the entire chain + leading to undefined behavior and may rewrite it as `unreachable`. Calling + `app_main` directly preserves the explicit OOB instruction and produces the + expected `out of bounds memory access` exception. + +2. **Cleaner trap point**: With `-f app_main`, the WAMR call stack starts at our app's + entry, not deep inside wasi-libc startup, making the symbolication output more + focused on user code. + +## Expected output + +### oob app + +``` +=== Running iwasm on oob.prod.wasm (expect crash) === + +#00: 0x1dd8 - app_main + +Exception: out of bounds memory access + +=== Captured call stack === +#00: 0x1dd8 - app_main + +=== Symbolicated call stack (using debug companion) === +0: do_bad_access (inlined into trigger_oob) + at .../wasm-apps/oob_access.c:11:15 + trigger_oob (inlined into app_main) + at .../wasm-apps/oob_main.c:17:5 + app_main + at .../wasm-apps/oob_main.c:23:5 +``` + +Although `do_bad_access` and `trigger_oob` were both inlined into `app_main` +under `-Oz -flto` (the runtime sees only a single WASM function), the debug +companion's DWARF retains `DW_TAG_inlined_subroutine` entries that describe +the inline chain. `addr2line.py` walks them and reports the trap site at all +three source levels. + +This works because the iwasm in this sample is built with +`WAMR_DISABLE_HW_BOUND_CHECK=1`: OOB memory access goes through the +interpreter's exception path (which captures `frame_ip` via +`SYNC_ALL_TO_FRAME`), not through a SIGSEGV signal handler that +longjmps out without updating ip. The AOT build uses `--bounds-checks=1` +for the same reason. + +### stackoverflow app + +``` +=== Running iwasm on stackoverflow.prod.wasm (expect crash) === + +#00: 0x1e15 - $f12 +#01: 0x1e15 - $f12 + ... (~20 identical frames as the wasm operand stack overflows) ... +#21: 0x1e15 - $f12 +#22: 0x1dd0 - app_main + +Exception: wasm operand stack overflow + +=== Captured call stack === +... same as above ... + +=== Symbolicated call stack (using debug companion) === +0: recurse + at .../wasm-apps/stackoverflow_recurse.c:20:13 + ... (one resolved frame per captured frame) ... +22: app_main + at .../wasm-apps/stackoverflow_main.c:14:5 +``` + +Stack overflow produces non-zero offsets at every frame (the runtime +captures the ip of the call instruction in each caller), so `addr2line.py` +resolves source file, line number, and function name correctly across the +recursive chain. `llvm-symbolizer`'s outermost frame's function name is +unreliable on wasm for two independent reasons: LLVM unconditionally +overrides that frame's name from an object-file symbol-table lookup +(which can return a wrong function on some wasm layouts), and +`wasm-opt -Oz -g` leaves dead-code-eliminated `DW_TAG_subprogram` DIEs +behind with `low_pc = 0` which then violate DWARF's DIE-range map +invariants. `addr2line.py` always applies an interval-table overlay +to the outermost frame's name, so it renders as `recurse` / `app_main` +in both cases. + +## Manual decode + +If you have a captured stack from another iwasm run (e.g., from a remote board or +saved log), you can symbolicate it directly: + +```bash +python3 ../../test-tools/addr2line/addr2line.py \ + --wasi-sdk /opt/wasi-sdk \ + --wabt /opt/wabt \ + --wasm-file build/wasm-apps/oob.debug.wasm \ + /path/to/saved/call_stack.txt +``` + +## Environment variables + +| Variable | Default | Used by | +|----------|---------|---------| +| `WASI_SDK_PATH` | `/opt/wasi-sdk` | Build (clang, llvm-strip) and decode (llvm-symbolizer, llvm-dwarfdump) | +| `BINARYEN_PATH` | `/opt/binaryen` | Build (wasm-opt) | +| `WABT_PATH` | `/opt/wabt` | Decode (wasm-objdump) | + +## References + +- [addr2line.py](../../test-tools/addr2line/addr2line.py) +- [WAMR Dump Call Stack Feature](../../doc/build_wamr.md#dump-call-stack-feature) +- [Zephyr coredump-debug sample](../../product-mini/platforms/zephyr/coredump-debug/) — same workflow on embedded +- [debug-tools sample](../debug-tools/) — non-optimized debug build for comparison diff --git a/samples/debug-tools-optimized/symbolicate.sh b/samples/debug-tools-optimized/symbolicate.sh new file mode 100755 index 0000000000..1b77fc897b --- /dev/null +++ b/samples/debug-tools-optimized/symbolicate.sh @@ -0,0 +1,109 @@ +#!/bin/bash +# Copyright (C) 2019 Intel Corporation. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +set -euo pipefail + +# Run a wasm or aot app, capture the WAMR call stack, and symbolicate it +# using addr2line.py with the debug companion. +# +# Usage: ./symbolicate.sh [oob|stackoverflow] [wasm|aot] + +SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) +APP="${1:-oob}" +MODE="${2:-wasm}" + +if [ "$APP" != "oob" ] && [ "$APP" != "stackoverflow" ]; then + echo "Usage: $0 [oob|stackoverflow] [wasm|aot]" >&2 + exit 1 +fi +if [ "$MODE" != "wasm" ] && [ "$MODE" != "aot" ]; then + echo "Usage: $0 [oob|stackoverflow] [wasm|aot]" >&2 + exit 1 +fi + +WASI_SDK_PATH="${WASI_SDK_PATH:-/opt/wasi-sdk}" +WABT_PATH="${WABT_PATH:-/opt/wabt}" +WAMR_ROOT="${SCRIPT_DIR}/../.." + +BUILD_DIR="${SCRIPT_DIR}/build" +if [ "$MODE" = "wasm" ]; then + PROD_FILE="${BUILD_DIR}/wasm-apps/${APP}.prod.wasm" +else + PROD_FILE="${BUILD_DIR}/wasm-apps/${APP}.prod.aot" +fi +DEBUG_WASM="${BUILD_DIR}/wasm-apps/${APP}.debug.wasm" +IWASM="${BUILD_DIR}/iwasm" + +if [ ! -x "${IWASM}" ]; then + echo "iwasm not found at ${IWASM}" >&2 + echo "Run: mkdir -p build && cd build && cmake .. && make" >&2 + exit 1 +fi +if [ ! -f "${PROD_FILE}" ]; then + echo "Production binary not found at ${PROD_FILE}" >&2 + exit 1 +fi +if [ ! -f "${DEBUG_WASM}" ]; then + echo "Debug companion not found at ${DEBUG_WASM}" >&2 + exit 1 +fi + +CALL_STACK_FILE=$(mktemp) +LOG_FILE=$(mktemp) +trap 'rm -f "${CALL_STACK_FILE}" "${LOG_FILE}"' EXIT + +echo "=== Running iwasm on ${APP}.prod.${MODE} (expect crash) ===" +# -f app_main calls the exported app_main directly, bypassing wasi _start. +# This preserves the OOB / stack-overflow trap behavior — running _start +# under -Oz -flto would lower the OOB pattern to `unreachable` and produce +# misleading call-stack info. +# +# stackoverflow uses --stack-size=4096 so the WASM operand stack overflows +# at a reasonable depth (~20 frames). With the default stack size the +# recursion would still trap, just much later (~400 frames) and with a +# much larger captured call-stack to symbolicate. +IWASM_ARGS=() +if [ "$APP" = "stackoverflow" ]; then + IWASM_ARGS+=(--stack-size=4096) +fi +"${IWASM}" "${IWASM_ARGS[@]}" -f app_main "${PROD_FILE}" 2>&1 | tee "${LOG_FILE}" || true + +echo "" +echo "=== Captured call stack ===" +grep -E "^#[0-9]+:" "${LOG_FILE}" > "${CALL_STACK_FILE}" || true +cat "${CALL_STACK_FILE}" + +if [ ! -s "${CALL_STACK_FILE}" ]; then + echo "(no call stack captured)" + exit 1 +fi + +echo "" +echo "=== Symbolicated call stack (using debug companion) ===" +# Pick the right --mode for addr2line.py: +# - aot: offsets are file-absolute, no adjustment (wamrc commits ip +# at instruction start). Always use --mode=aot regardless of +# how iwasm itself was built. +# - wasm + classic interp: offsets are file-absolute, post-advance → --mode=interp +# - wasm + fast interp: offsets are function-relative in transformed +# bytecode → --mode=fast-interp (function-name lookup only) +# +# Detect fast-interp by inspecting CMakeCache.txt for WAMR_BUILD_FAST_INTERP=1. +if [ "$MODE" = "aot" ]; then + A2L_MODE=aot +else + A2L_MODE=interp + # Detect fast-interp by inspecting iwasm for the wasm_interp_fast.c symbol. + if strings "${IWASM}" 2>/dev/null | grep -q "wasm_interp_fast.c"; then + A2L_MODE=fast-interp + fi +fi + +# DWARF only lives in the .debug.wasm — addr2line.py uses it for all modes. +python3 "${WAMR_ROOT}/test-tools/addr2line/addr2line.py" \ + --wasi-sdk "${WASI_SDK_PATH}" \ + --wabt "${WABT_PATH}" \ + --wasm-file "${DEBUG_WASM}" \ + --mode "${A2L_MODE}" \ + "${CALL_STACK_FILE}" diff --git a/samples/debug-tools-optimized/verify.sh b/samples/debug-tools-optimized/verify.sh new file mode 100755 index 0000000000..e77b14cede --- /dev/null +++ b/samples/debug-tools-optimized/verify.sh @@ -0,0 +1,108 @@ +#!/bin/bash +# Copyright (C) 2019 Intel Corporation. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# Verify symbolicated output for one (app, mode) combination. +# +# Usage: ./verify.sh +# +# Runs ./symbolicate.sh and asserts the output contains the expected source +# file references. Auto-detects whether iwasm was built with classic or fast +# interpreter (via the symbolicate.sh script itself) and relaxes the +# fast-interp + oob + wasm assertion since fast-interp can't resolve +# offset=0 (trap-at-entry) to a source line. + +set -euo pipefail + +SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) +APP="${1:-}" +MODE="${2:-}" + +if [ "$APP" != "oob" ] && [ "$APP" != "stackoverflow" ]; then + echo "Usage: $0 " >&2 + exit 2 +fi +if [ "$MODE" != "wasm" ] && [ "$MODE" != "aot" ]; then + echo "Usage: $0 " >&2 + exit 2 +fi + +# Detect interpreter mode by checking the iwasm binary (same logic as symbolicate.sh) +IWASM="${SCRIPT_DIR}/build/iwasm" +if [ ! -x "$IWASM" ]; then + echo "iwasm not found at $IWASM; build the sample first" >&2 + exit 1 +fi +INTERP="classic" +if strings "$IWASM" 2>/dev/null | grep -q "wasm_interp_fast.c"; then + INTERP="fast" +fi + +OUT=$(mktemp) +trap 'rm -f "$OUT"' EXIT + +"${SCRIPT_DIR}/symbolicate.sh" "$APP" "$MODE" 2>&1 | tee "$OUT" > /dev/null + +assert() { + local pattern="$1" + if ! grep -q "$pattern" "$OUT"; then + echo "FAIL [$INTERP/$APP/$MODE]: pattern '$pattern' not found in output" >&2 + cat "$OUT" >&2 + exit 1 + fi +} + +# assert_re: assert a POSIX extended regex matches somewhere in the captured output. +# Used for compound expectations like "function name X on its own line". +assert_re() { + local pattern="$1" + if ! grep -Eq "$pattern" "$OUT"; then + echo "FAIL [$INTERP/$APP/$MODE]: regex '$pattern' did not match output" >&2 + cat "$OUT" >&2 + exit 1 + fi +} + +case "$APP" in + oob) + # Runtime always reports the OOB exception type. + assert "out of bounds memory access" + # do_bad_access -> trigger_oob -> app_main are all inlined into a + # single WASM function under -Oz -flto. On classic-interp + wasm + # and aot builds, DWARF retains the inline chain, so addr2line.py + # emits all three names with "(inlined into )" annotations. + # On fast-interp + wasm the runtime offset doesn't map to source + # (transformed bytecode), so addr2line.py falls back to + # function-name lookup and emits only the outermost frame. + if [ "$INTERP" = "fast" ] && [ "$MODE" = "wasm" ]; then + assert_re '^0: app_main$' + else + assert_re '^0: do_bad_access \(inlined into trigger_oob\)$' + assert_re '^[[:space:]]+trigger_oob \(inlined into app_main\)$' + assert_re '^[[:space:]]+app_main$' + assert "oob_access.c" + assert "oob_main.c" + fi + ;; + stackoverflow) + # stackoverflow_recurse.c uses non-tail recursion that reliably + # exhausts the WASM operand stack (with --stack-size=4096 in + # symbolicate.sh, depth lands around 20 frames). + assert "wasm operand stack overflow" + # The captured call stack is recurse repeated, ending in app_main. + # addr2line.py must resolve both function names exactly. + assert_re '^[0-9]+: recurse$' + assert "stackoverflow_recurse.c" + if [ "$INTERP" = "fast" ] && [ "$MODE" = "wasm" ]; then + # fast-interp falls back to function-name lookup; the + # outermost app_main frame may not surface stackoverflow_main.c + # but the function name itself does. + assert_re '^[0-9]+: app_main$' + else + assert_re '^[0-9]+: app_main$' + assert "stackoverflow_main.c" + fi + ;; +esac + +echo "PASS [$INTERP/$APP/$MODE]" diff --git a/samples/debug-tools-optimized/wasm-apps/CMakeLists.txt b/samples/debug-tools-optimized/wasm-apps/CMakeLists.txt new file mode 100644 index 0000000000..b2a56b0f6d --- /dev/null +++ b/samples/debug-tools-optimized/wasm-apps/CMakeLists.txt @@ -0,0 +1,119 @@ +# Copyright (C) 2026 Intel Corporation. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +cmake_minimum_required(VERSION 3.14) +project(debug_tools_optimized_wasm_apps LANGUAGES NONE) + +set(WAMR_ROOT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../..") + +# --- wasi-sdk tool discovery --- +if(NOT DEFINED WASI_SDK_PATH) + if(DEFINED ENV{WASI_SDK_PATH}) + set(WASI_SDK_PATH "$ENV{WASI_SDK_PATH}") + else() + set(WASI_SDK_PATH "/opt/wasi-sdk") + endif() +endif() + +set(WASI_SDK_CLANG "${WASI_SDK_PATH}/bin/clang") +set(WASI_SDK_STRIP "${WASI_SDK_PATH}/bin/llvm-strip") + +if(NOT EXISTS "${WASI_SDK_CLANG}") + message(FATAL_ERROR "wasi-sdk clang not found at ${WASI_SDK_CLANG}.") +endif() +if(NOT EXISTS "${WASI_SDK_STRIP}") + message(FATAL_ERROR "llvm-strip not found at ${WASI_SDK_STRIP}.") +endif() + +# --- binaryen (wasm-opt) discovery --- +if(NOT DEFINED BINARYEN_PATH) + if(DEFINED ENV{BINARYEN_PATH}) + set(BINARYEN_PATH "$ENV{BINARYEN_PATH}") + else() + set(BINARYEN_PATH "/opt/binaryen") + endif() +endif() + +set(WASM_OPT "${BINARYEN_PATH}/bin/wasm-opt") +if(NOT EXISTS "${WASM_OPT}") + message(FATAL_ERROR "wasm-opt not found at ${WASM_OPT}. Set BINARYEN_PATH.") +endif() + +# --- wamrc discovery (for AOT) --- +list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/../../cmake) +find_package(WAMRC REQUIRED) + +set(WASM_COMPILE_FLAGS + --target=wasm32-wasi + -Oz -g -flto + -Wl,--export=app_main +) + +# Build a wasm app from one or more source files. +# Usage: build_wasm_app( [ ...]) +function(build_wasm_app APP_NAME) + set(SOURCES ${ARGN}) + + set(SOURCE_PATHS "") + foreach(SRC ${SOURCES}) + list(APPEND SOURCE_PATHS "${CMAKE_CURRENT_SOURCE_DIR}/${SRC}") + endforeach() + + set(WASM_INTERMEDIATE "${CMAKE_CURRENT_BINARY_DIR}/${APP_NAME}.wasm") + set(WASM_DEBUG "${CMAKE_CURRENT_BINARY_DIR}/${APP_NAME}.debug.wasm") + set(WASM_PROD "${CMAKE_CURRENT_BINARY_DIR}/${APP_NAME}.prod.wasm") + set(AOT_PROD "${CMAKE_CURRENT_BINARY_DIR}/${APP_NAME}.prod.aot") + + # 1. Compile all sources together with -Oz -g -flto + add_custom_command( + OUTPUT "${WASM_INTERMEDIATE}" + COMMAND ${WASI_SDK_CLANG} ${WASM_COMPILE_FLAGS} + -o "${WASM_INTERMEDIATE}" + ${SOURCE_PATHS} + DEPENDS ${SOURCE_PATHS} + COMMENT "[${APP_NAME}] Compiling -> ${APP_NAME}.wasm (intermediate, -Oz -g -flto)" + ) + + # 2. Debug companion (preserves DWARF + name section) + add_custom_command( + OUTPUT "${WASM_DEBUG}" + COMMAND ${WASM_OPT} -Oz -g -o "${WASM_DEBUG}" "${WASM_INTERMEDIATE}" + DEPENDS "${WASM_INTERMEDIATE}" + COMMENT "[${APP_NAME}] wasm-opt -Oz -g -> ${APP_NAME}.debug.wasm (companion)" + ) + + # 3. Production (strip debug companion to guarantee identical code) + add_custom_command( + OUTPUT "${WASM_PROD}" + COMMAND ${WASI_SDK_STRIP} --strip-all -o "${WASM_PROD}" "${WASM_DEBUG}" + DEPENDS "${WASM_DEBUG}" + COMMENT "[${APP_NAME}] strip -> ${APP_NAME}.prod.wasm (production)" + ) + + # 4. AOT (compiled from production wasm with --enable-dump-call-stack) + # The .debug.wasm companion is still used for symbolication — DWARF only + # lives in the wasm; AOT carries the call-stack metadata only. + # --bounds-checks=1 forces explicit memory bounds checks instead of + # relying on hardware traps; this matches the iwasm build (which + # disables HW bound checks so OOB traps go through the interpreter + # exception path and capture frame_ip — without it, the AOT path + # would SIGSEGV with no captured call stack and inline DWARF info + # would be unrecoverable on the OOB sample. + add_custom_command( + OUTPUT "${AOT_PROD}" + DEPENDS ${WAMRC_BIN} "${WASM_PROD}" + COMMAND ${WAMRC_BIN} --size-level=0 --enable-dump-call-stack + --bounds-checks=1 + -o "${AOT_PROD}" "${WASM_PROD}" + COMMENT "[${APP_NAME}] wamrc -> ${APP_NAME}.prod.aot (AOT)" + ) + + add_custom_target(${APP_NAME}_wasm ALL + DEPENDS "${WASM_INTERMEDIATE}" "${WASM_DEBUG}" "${WASM_PROD}" "${AOT_PROD}" + ) + + install(FILES "${WASM_DEBUG}" "${WASM_PROD}" "${AOT_PROD}" DESTINATION wasm-apps) +endfunction() + +build_wasm_app(oob oob_main.c oob_access.c) +build_wasm_app(stackoverflow stackoverflow_main.c stackoverflow_recurse.c) diff --git a/samples/debug-tools-optimized/wasm-apps/oob_access.c b/samples/debug-tools-optimized/wasm-apps/oob_access.c new file mode 100644 index 0000000000..ccb08a2943 --- /dev/null +++ b/samples/debug-tools-optimized/wasm-apps/oob_access.c @@ -0,0 +1,12 @@ +/* + * Copyright (C) 2019 Intel Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ + +void +do_bad_access(int offset) +{ + volatile int *p = (volatile int *)0; + /* Write to an address way beyond linear memory to trigger OOB trap */ + p[offset] = 0xDEAD; +} diff --git a/samples/debug-tools-optimized/wasm-apps/oob_main.c b/samples/debug-tools-optimized/wasm-apps/oob_main.c new file mode 100644 index 0000000000..30a8b5e948 --- /dev/null +++ b/samples/debug-tools-optimized/wasm-apps/oob_main.c @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2019 Intel Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ + +/* Deliberate out-of-bounds memory access for coredump debug demo. + Multi-file: app_main -> trigger_oob (this file) -> do_bad_access + (oob_access.c) Under -Oz, these get inlined into app_main. The debug + companion retains DWARF inline info so the full chain can be recovered + offline. */ + +void +do_bad_access(int offset); + +void +trigger_oob(void) +{ + /* 0x7FFFFFFF is well beyond any WASM linear memory */ + do_bad_access(0x7FFFFFFF); +} + +void +app_main(void) +{ + trigger_oob(); +} diff --git a/samples/debug-tools-optimized/wasm-apps/stackoverflow_main.c b/samples/debug-tools-optimized/wasm-apps/stackoverflow_main.c new file mode 100644 index 0000000000..0e57a2e640 --- /dev/null +++ b/samples/debug-tools-optimized/wasm-apps/stackoverflow_main.c @@ -0,0 +1,16 @@ +/* + * Copyright (C) 2019 Intel Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ + +/* Recursive stack overflow for coredump debug demo. + Multi-file: app_main (this file) -> recurse (stackoverflow_recurse.c) */ + +int +recurse(int depth); + +void +app_main(void) +{ + recurse(0); +} diff --git a/samples/debug-tools-optimized/wasm-apps/stackoverflow_recurse.c b/samples/debug-tools-optimized/wasm-apps/stackoverflow_recurse.c new file mode 100644 index 0000000000..0cbf7a37bf --- /dev/null +++ b/samples/debug-tools-optimized/wasm-apps/stackoverflow_recurse.c @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2019 Intel Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ + +static volatile int sink; + +int +recurse(int depth) +{ + /* Allocate stack space each frame to accelerate overflow */ + volatile char buf[128]; + buf[0] = (char)depth; + buf[127] = (char)(depth >> 8); + sink = buf[0] + buf[127]; + if (depth == 10000) { + __builtin_trap(); + } + /* TODO: Use return value after the call to prevent tail-call optimization, + * it will stack overflow */ + int r = recurse(depth + 1); + return r + buf[0]; + /* TODO: will optimize to loop, and it will trap when reach certain level */ + // return recurse(depth + 1); +} diff --git a/samples/debug-tools/README.md b/samples/debug-tools/README.md index b0358b9e4f..7ed69b0882 100644 --- a/samples/debug-tools/README.md +++ b/samples/debug-tools/README.md @@ -52,25 +52,104 @@ $ python3 ../../../test-tools/addr2line/addr2line.py \ call_stack.txt ``` +This sample also ships `symbolicate.sh` (runs the three modes shown below in +sequence) and `verify.sh` (asserts inline expansion appears in both the +classic-interp and AOT outputs — used by CI, useful locally as a smoke test). +Both expect to be run from the sample root after building: + +```bash +$ ./symbolicate.sh +$ ./verify.sh +``` + The output should be something like: ```text -0: c - at wasm-micro-runtime/samples/debug-tools/wasm-apps/trap.c:5:1 +0: trap_helper (inlined into c) + at /path/to/wasm-micro-runtime/samples/debug-tools/wasm-apps/trap.c:9:5 + c + at /path/to/wasm-micro-runtime/samples/debug-tools/wasm-apps/trap.c:16:12 1: b - at wasm-micro-runtime/samples/debug-tools/wasm-apps/trap.c:11:12 + at /path/to/wasm-micro-runtime/samples/debug-tools/wasm-apps/trap.c:23:12 2: a - at wasm-micro-runtime/samples/debug-tools/wasm-apps/trap.c:17:12 + at /path/to/wasm-micro-runtime/samples/debug-tools/wasm-apps/trap.c:29:12 3: main - at wasm-micro-runtime/samples/debug-tools/wasm-apps/trap.c:24:5 + at /path/to/wasm-micro-runtime/samples/debug-tools/wasm-apps/trap.c:36:5 4: __main_void - at unknown:?:? + at ??:0 5: _start + at wasisdk://v25.0/build/sysroot/wasi-libc-wasm32-wasi/libc-bottom-half/crt/crt1-command.c:43:13 ``` -If WAMR is run in fast interpreter mode (`WAMR_BUILD_FAST_INTERP=1`), addresses in the stack trace cannot be tracked back to location info. -If WAMR <= `1.3.2` is used, the stack trace does not contain addresses. -In those two cases, run the script with `--no-addr`: the line info returned refers to the start of the function +Frame `0` shows **inline expansion** in action — `trap_helper` (the actual trap site) +and `c` (its caller) appear together under index `0` because `trap_helper` was inlined +into `c` by `__attribute__((always_inline))`. The `(inlined into c)` suffix on +`trap_helper` makes the relationship explicit: the runtime saw one WASM frame for `c`, +but addr2line.py reconstructs the full source-level call chain from DWARF +`DW_TAG_inlined_subroutine` entries. Each frame in the chain except the outermost +gets the `(inlined into )` annotation. + +### Inline expansion + +addr2line.py automatically expands inline call chains when the binary's DWARF contains +`DW_TAG_inlined_subroutine` entries. This happens when functions are inlined either by +optimization (e.g. `-O2`, `-Oz`) or by `__attribute__((always_inline))`. + +The included `trap.c` marks `trap_helper` as always_inline and places `__builtin_trap()` +inside it. The trap address therefore falls within the inlined region, producing the +multi-line frame `0` shown above. Each inlined frame's source location is reported +independently, so you can see exactly which inlined call chain led to the trap. + +Inline expansion requires no flags — it's automatic whenever the DWARF carries the +relevant inline metadata. + +For details on the tools `addr2line.py` orchestrates internally and why each +is needed, see [`test-tools/addr2line/README.md`](../../test-tools/addr2line/README.md). +The longer-form discussion of the interval-table overlay — applied to correct +the LLVM symbolizer's outermost function-name lookup on wasm — also lives there. + +### Execution modes — `--mode={interp,aot,fast-interp}` + +Different WAMR execution modes report call-stack offsets in different address spaces. +Pass the right `--mode` to addr2line.py so offsets are interpreted correctly: + +| Mode | When | Adjustment | Source resolution | +|------|------|-----------|-------------------| +| `interp` (default) | Classic interpreter (`WAMR_BUILD_FAST_INTERP=0`) | `offset - code_start - 1` (post-advance) | Full file:line, inline expansion | +| `aot` | AOT-compiled .aot files | `offset - code_start` (wamrc commits at instruction start) | Full file:line, inline expansion | +| `fast-interp` | Fast interpreter (`WAMR_BUILD_FAST_INTERP=1`) | (not mappable) | Function-name only | + +For AOT call stacks: + +```bash +$ python3 ../../../test-tools/addr2line/addr2line.py \ + --wasi-sdk /opt/wasi-sdk \ + --wabt /opt/wabt \ + --wasm-file wasm-apps/trap.wasm \ + --mode aot \ + call_stack_aot.txt +``` + +For fast-interp call stacks: + +```bash +$ python3 ../../../test-tools/addr2line/addr2line.py \ + --wasi-sdk /opt/wasi-sdk \ + --wabt /opt/wabt \ + --wasm-file wasm-apps/trap.wasm \ + --mode fast-interp \ + call_stack.txt +``` + +Fast-interp transforms the WASM bytecode in-memory at load time (replaces opcodes +with handler indices, expands LEBs to fixed widths). The runtime ip therefore +points into the *transformed* buffer — there's no way to map an offset back to a +source line. `--mode=fast-interp` falls back to function-name lookup (equivalent +to `--no-addr`). + +If WAMR <= `1.3.2` is used, the stack trace does not contain addresses at all. +In that case, run the script with `--no-addr`: the line info returned refers to +the start of the function. ```bash $ python3 ../../../test-tools/addr2line/addr2line.py \ diff --git a/samples/debug-tools/symbolicate.sh b/samples/debug-tools/symbolicate.sh old mode 100644 new mode 100755 index 709622f03d..e5a43e4c7e --- a/samples/debug-tools/symbolicate.sh +++ b/samples/debug-tools/symbolicate.sh @@ -1,23 +1,52 @@ #!/bin/bash -set -euox pipefail - -# Symbolicate .wasm -python3 ../../../test-tools/addr2line/addr2line.py \ - --wasi-sdk /opt/wasi-sdk \ - --wabt /opt/wabt \ - --wasm-file wasm-apps/trap.wasm \ - call_stack.txt - -# Symbolicate .wasm with `--no-addr` -python3 ../../../test-tools/addr2line/addr2line.py \ - --wasi-sdk /opt/wasi-sdk \ - --wabt /opt/wabt \ - --wasm-file wasm-apps/trap.wasm \ - call_stack.txt --no-addr - -# Symbolicate .aot -python3 ../../../test-tools/addr2line/addr2line.py \ - --wasi-sdk /opt/wasi-sdk \ - --wabt /opt/wabt \ - --wasm-file wasm-apps/trap.wasm \ - call_stack_aot.txt \ No newline at end of file +# Copyright (C) 2019 Intel Corporation. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +set -euo pipefail + +# Symbolicate the captured call stack files (call_stack.txt for .wasm, +# call_stack_aot.txt for .aot) located in this sample's build/ directory. +# Self-locating so invocation works from any cwd. + +SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) +BUILD_DIR="${SCRIPT_DIR}/build" +ADDR2LINE="${SCRIPT_DIR}/../../test-tools/addr2line/addr2line.py" +WASM_FILE="${BUILD_DIR}/wasm-apps/trap.wasm" +CALL_STACK="${BUILD_DIR}/call_stack.txt" +CALL_STACK_AOT="${BUILD_DIR}/call_stack_aot.txt" + +WASI_SDK_PATH="${WASI_SDK_PATH:-/opt/wasi-sdk}" +WABT_PATH="${WABT_PATH:-/opt/wabt}" + +banner() { + echo + echo "===== $* =====" +} + +# .wasm + classic-interp: addr2line.py defaults to --mode=interp. +banner "wasm / --mode=interp (default; classic interpreter, post-advance offsets)" +python3 "$ADDR2LINE" \ + --wasi-sdk "$WASI_SDK_PATH" \ + --wabt "$WABT_PATH" \ + --wasm-file "$WASM_FILE" \ + "$CALL_STACK" + +# Same call stack with --no-addr: function-level resolution only — use +# this for fast-interp call stacks (transformed bytecode has no source +# mapping) and for very old iwasm dumps that don't carry offsets. +banner "wasm / --no-addr (function-name lookup only; fast-interp & WAMR<=1.3.2 fallback)" +python3 "$ADDR2LINE" \ + --wasi-sdk "$WASI_SDK_PATH" \ + --wabt "$WABT_PATH" \ + --wasm-file "$WASM_FILE" \ + --no-addr \ + "$CALL_STACK" + +# .aot: --mode=aot — wamrc commits ip at instruction-start (not post-advance). +banner "aot / --mode=aot (instruction-start offsets)" +python3 "$ADDR2LINE" \ + --wasi-sdk "$WASI_SDK_PATH" \ + --wabt "$WABT_PATH" \ + --wasm-file "$WASM_FILE" \ + --mode aot \ + "$CALL_STACK_AOT" diff --git a/samples/debug-tools/verify.sh b/samples/debug-tools/verify.sh new file mode 100755 index 0000000000..59a148f70e --- /dev/null +++ b/samples/debug-tools/verify.sh @@ -0,0 +1,83 @@ +#!/bin/bash +# Copyright (C) 2019 Intel Corporation. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# Verify symbolicated output for samples/debug-tools. +# +# Usage: ./verify.sh +# +# Captures call stacks for both .wasm and .aot, runs symbolicate.sh, and +# asserts that the inline expansion of trap_helper (always_inline) shows +# up under both --mode=interp (the default for the .wasm path) and +# --mode=aot (the third invocation in symbolicate.sh). + +set -euo pipefail + +SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) +BUILD_DIR="${SCRIPT_DIR}/build" + +if [ ! -x "${BUILD_DIR}/iwasm" ]; then + echo "iwasm not found at ${BUILD_DIR}/iwasm; build the sample first" >&2 + exit 1 +fi + +cd "${BUILD_DIR}" +./iwasm wasm-apps/trap.wasm 2>&1 | grep "^#" > call_stack.txt || true +./iwasm wasm-apps/trap.aot 2>&1 | grep "^#" > call_stack_aot.txt || true + +OUT=$(mktemp) +trap 'rm -f "$OUT"' EXIT +"${SCRIPT_DIR}/symbolicate.sh" 2>&1 | tee "$OUT" > /dev/null + +assert() { + local pattern="$1" + local where="$2" + if ! grep -q "$pattern" "$OUT"; then + echo "FAIL: pattern '$pattern' ($where) not found in symbolicate.sh output" >&2 + cat "$OUT" >&2 + exit 1 + fi +} + +# assert_re: like assert but with POSIX extended regex (anchored line match). +assert_re() { + local pattern="$1" + local where="$2" + if ! grep -Eq "$pattern" "$OUT"; then + echo "FAIL: regex '$pattern' ($where) did not match symbolicate.sh output" >&2 + cat "$OUT" >&2 + exit 1 + fi +} + +# trap.c's runtime call chain is _start -> ... -> main -> a -> b -> c +# (with c calling __builtin_trap inside trap_helper, an always_inline +# helper). Symbolicate.sh runs three invocations against the same call +# stack. We assert the precise frames each one produces. +# +# All invocations should symbolicate the user-code frames `c`, `b`, `a`, +# `main` (each on its own line, prefixed by the frame index). +assert_re '^[0-9]+: c$' "user frame: c" +assert_re '^[0-9]+: b$' "user frame: b" +assert_re '^[0-9]+: a$' "user frame: a" +assert_re '^[0-9]+: main$' "user frame: main" +assert "trap.c" "source file" + +# Inline expansion of trap_helper (always_inline) only surfaces in the +# address-based runs (default --mode=interp and --mode=aot). It MUST +# appear at least once in the captured output, with the +# "(inlined into c)" annotation that print_frames adds for inlined +# frames. +assert_re '^[[:space:]]*0:[[:space:]]+trap_helper[[:space:]]+\(inlined into c\)$' \ + "inline expansion: trap_helper (inlined into c)" + +# The AOT block is the third (last) invocation in symbolicate.sh. +# Confirm --mode=aot also produces inline expansion (the outermost +# frame at index 0 should still be trap_helper inlined into c). +if ! tail -25 "$OUT" | grep -Eq '^[[:space:]]*0:[[:space:]]+trap_helper[[:space:]]+\(inlined into c\)$'; then + echo "FAIL: AOT block (last 25 lines) missing 'trap_helper (inlined into c)' — --mode=aot may be broken" >&2 + cat "$OUT" >&2 + exit 1 +fi + +echo "PASS [debug-tools symbolication: precise frames + inline expansion verified for both wasm and aot]" diff --git a/samples/debug-tools/wasm-apps/CMakeLists.txt b/samples/debug-tools/wasm-apps/CMakeLists.txt index 9858b98dab..82ea845c1b 100644 --- a/samples/debug-tools/wasm-apps/CMakeLists.txt +++ b/samples/debug-tools/wasm-apps/CMakeLists.txt @@ -34,6 +34,9 @@ function (compile_sample SOURCE_FILE) COMMAND ${WAMRC_BIN} --size-level=0 --enable-dump-call-stack -o ${AOT_FILE} ${WASM_FILE} WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} ) + # Force the .wasm producer to run before the AOT step under `make -j`; + # the DEPENDS line above only names the file, not the CMake target. + add_dependencies(${FILE_NAME}_aot ${FILE_NAME}) ## wasm + sourcemap if (DEFINED EMSCRIPTEN) diff --git a/samples/debug-tools/wasm-apps/trap.c b/samples/debug-tools/wasm-apps/trap.c index 364c430d19..27a5d0245b 100644 --- a/samples/debug-tools/wasm-apps/trap.c +++ b/samples/debug-tools/wasm-apps/trap.c @@ -1,7 +1,18 @@ +__attribute__((always_inline)) static inline int +trap_helper(int n) +{ + /* Forced-inline helper to demonstrate inline call stack expansion. + The trap is inside this helper so the runtime offset falls within + the inlined region — addr2line.py will then show both trap_helper + and c() in the call chain (innermost first). */ + __builtin_trap(); + return n + 100; +} + int c(int n) { - __builtin_trap(); + return trap_helper(n); } int diff --git a/test-tools/addr2line/README.md b/test-tools/addr2line/README.md new file mode 100644 index 0000000000..0254912810 --- /dev/null +++ b/test-tools/addr2line/README.md @@ -0,0 +1,168 @@ +# `addr2line.py` — symbolicating WAMR call stacks + +`addr2line.py` turns a WAMR call-stack dump (`#00: 0x0040 - $f0` lines) back +into source-level information (function names, files, line numbers, inline +chains). It is a thin orchestrator over four LLVM/wabt tools, with workarounds +for two LLVM symbolization issues on wasm targets (one a design choice, one an +invariant violation from `wasm-opt -g`). + +The two `samples/debug-tools*/` samples are end-to-end demos of the workflow. +This README focuses on the tool itself: how it works, why each subprocess is +necessary, and why the interval-table overlay is required. + +## Quick reference + +```bash +python3 addr2line.py \ + --wasi-sdk /opt/wasi-sdk \ + --wabt /opt/wabt \ + --wasm-file /path/to/app.debug.wasm \ + call_stack.txt +``` + +| Flag | Default | Purpose | +|------|---------|---------| +| `--mode={interp,aot,fast-interp}` | `interp` | How to interpret runtime offsets — see *Three offset spaces* below. | +| `--no-addr` | off | Resolve by function name only (for very old iwasm dumps that lack offsets, or for fast-interp). | + +## What the script does, step by step + +1. **Parse each captured frame** (`#NN: 0xADDR - SYMBOL`). +2. **Convert the runtime offset to a DWARF address.** WAMR reports + file-absolute byte offsets; DWARF addresses are relative to the start of + the WASM Code section, so the script subtracts `code_section_start` (read + from `wasm-objdump -h`). +3. **Apply a mode-dependent adjustment.** See *Three offset spaces* below. +4. **Resolve the address.** `llvm-symbolizer -f -i` for line info and inline + chain, with the outermost frame's function name overlaid from a startup- + built interval table (see "Why the interval-table overlay exists" below). +5. **Demangle and render.** C++ symbols pass through `llvm-cxxfilt`; inline + frames are annotated `(inlined into )` so the chain reads top-down. + +## Three offset spaces (`--mode`) + +WAMR captures `frame_ip` differently in each execution mode, so the offset in +a captured frame means different things: + +| Mode | Offset space | Adjustment | What addr2line does | +|------|--------------|-----------|----------------------| +| `interp` (default) | File-absolute, *post*-advance — interpreter has already incremented `frame_ip` past the trapping opcode (and past LEB reads inside the handler) before the trap fires. | `offset - code_start - 1` | Full symbolication: file, line, column, inline chain. | +| `aot` | File-absolute, *instruction-start* — `wamrc` emits a `commit_ip` before every WASM operation. | `offset - code_start` | Full symbolication. | +| `fast-interp` | Function-relative, into the *transformed* in-memory bytecode (opcodes replaced with handler indices, LEBs expanded to fixed widths). The mapping back to source is destroyed by the transform. | (none) | Function-name lookup only — equivalent to `--no-addr`. One frame per input; no inline expansion (the `DW_TAG_inlined_subroutine` overlay is address-keyed and unavailable here). | + +Picking the wrong `--mode` lands the resolver inside an adjacent function and +silently produces wrong file/line. + +## Why the interval-table overlay exists + +`llvm-symbolizer -e -f -i ` is the natural way to map an address +to a function + source location, with `-i` expanding inline frames. It's +correct for line/column info and for inline frame names — but its +**outermost frame's function name** goes through two paths that can both +produce wrong answers on wasm: + +1. **Symbol-table override on the outermost frame.** `symbolizeInlinedCode` + unconditionally rewrites the outermost frame's `FunctionName` from + `getNameFromSymbolTable` whenever `FunctionNameKind::LinkageName` and + `UseSymbolTable` are both set — which is the default. Inline frames are + provably NOT rewritten (the override targets only + `getMutableFrame(getNumberOfFrames() - 1)`). On some wasi-sdk / wasm + layouts the symbol table's lookup returns the wrong function for an + address that DWARF places correctly. This is an intentional LLVM design + choice, not a bug — it exists because `-gline-tables-only` builds have + only line tables in DWARF and the symbol table gives better linkage + names in that case. The tradeoff bites on full-DWARF wasm builds. +2. **`AddrDieMap` invariant violation from `wasm-opt` DCE ghosts.** + `wasm-opt -Oz -g` leaves DIEs for dead-code-eliminated compiler-rt / + libm helpers behind with `low_pc = 0` and small `high_pc` (typically + `[0, 0)` or `[0, 6)`, sometimes up to `[0, 0x80)`). This is a + consequence of binaryen's DWARF preservation model: its address- + rewrite pass in `src/wasm/wasm-debug.cpp`'s + `LocationUpdater::getNewFuncStart` returns `0` as a tombstone when + an old address doesn't map to any surviving IR node, and `updateDIE` + writes that `0` into the `DW_AT_low_pc` — but leaves the + `DW_TAG_subprogram` in place, and sometimes leaves the original + `DW_AT_high_pc`. Binaryen's README calls this out: DWARF support is + optional, tracks address relocation rather than DIE-tree + restructuring, and is "not suitable for a fully optimized release + build." LLVM's `updateAddressDieMap` in `DWARFUnit.cpp` assumes + children are inserted after parents and each child range ⊆ + parent range; the ghost DIEs violate that invariant and the map's + `upper_bound − 1` lookup returns an insertion-order-dependent DIE. + +Empirical truth table (probing an inlined address of `trigger_oob` in a +small test module across all four toolchain combinations): + +| wasi-sdk | wasm-opt -Oz -g | symbolizer outer | mechanism | +|---|---|---|---| +| 29 (clang 21) | no | `free` ❌ | Symbol-table override | +| 29 (clang 21) | yes | `free` ❌ | Symbol-table override | +| 33 (clang 22) | no | `app_main` ✅ | Neither mechanism triggers | +| 33 (clang 22) | yes | `fmaf` ❌ | AddrDieMap invariant violated by a `[low_pc=0, 0x72)` `fmaf` ghost | + +The overlay fixes all four cases. Inner (inlined) frames are not affected +because they come from a different LLVM code path +(`getInliningInfoForAddress`), which walks `getParent()` links from the +leaf DIE and does NOT go through the symbol-table override applied to +the outermost concrete frame. + +### How the overlay works + +At startup, addr2line.py runs one `llvm-dwarfdump --debug-info` pass and +builds an in-memory list `[(low_pc, high_pc, name), ...]` covering every +real callable `DW_TAG_subprogram`. Three skip guards keep the list clean: + +- `DW_AT_declaration=true` → forward declarations (e.g. WASI imports like + `__imported_wasi_snapshot_preview1_*`) have no code, so they're excluded. +- `low_pc == 0` → structurally impossible for real code, since the wasm + binary spec guarantees the Code section begins after at least a Type + + Function section header. Any `low_pc = 0` DIE is a wasm-opt DCE ghost + or a broken producer's output. +- `high_pc <= low_pc` → empty or degenerate range. Defensive; never + observed on wasi-sdk 29 or 33. + +For each captured call-stack address, `lookup_subprogram_name` returns +the innermost (smallest range) surviving DW_TAG_subprogram covering the +address. That result overwrites the outermost frame's function name in +the symbolizer output; inner frame names are left alone. + +## Tools used by addr2line.py + +| Tool | From | Why this tool, and not another | +|------|------|--------------------------------| +| `llvm-symbolizer` | wasi-sdk | The actual address → `(function, file:line:column)` resolver, with inline expansion via `-i`. Ships in wasi-sdk 29+ (the project's minimum supported version). | +| `llvm-dwarfdump` | wasi-sdk | Single `--debug-info` pass at startup builds a `(low_pc, high_pc, name)` interval table for every real callable `DW_TAG_subprogram`. Used to overlay the outermost frame's function name in every resolution. The overlay is necessary because `llvm-symbolizer -f` unconditionally rewrites that frame's `FunctionName` from an object-file symbol-table lookup (LLVM design, not a bug) and — separately — `wasm-opt -Oz -g` output can violate the `AddrDieMap` invariant so DWARF's own DIE lookup returns the wrong entry. Reading DWARF ranges directly bypasses both mechanisms. No equivalent CLI exposes this lookup as structured output, so we parse the text. | +| `wasm-objdump` | wabt | Reports the Code-section start offset (needed to convert file-absolute runtime offsets to DWARF addresses) and the function-index → name table (a last-resort fallback for declaration-only DW_TAG_subprograms like `__main_void`). | +| `llvm-cxxfilt` | wasi-sdk | C++ symbol demangling, applied as a final pass on resolved names. | + +The script also detects an emscripten-style sourceMappingURL section and +falls back to `emsymbolizer` for that case. + +## Behavior matrix + +| Input | Resolver path | Output | +|-------|---------------|--------| +| `--mode=interp` or `--mode=aot`, address in DWARF range | Unified | `llvm-symbolizer -f -i` for line info + inline chain; outermost frame's function name overlaid from a startup-built interval table (innermost match wins) | +| `--mode=fast-interp` | Function-name | Single frame per input, no inline expansion, no source mapping | +| `--no-addr` | Function-name | Resolves the *function* — file:line refers to its declaration, not the trap site | +| Offset = 0 | Function-name fallback (with `(offset=0 — function entry)` note) | Used when WAMR couldn't capture `frame_ip` (e.g., trap at function entry, or top frame of an `iwasm -f` invocation) | +| Address outside any DW_TAG_subprogram | wasm-objdump function-index table | Single name; useful for declaration-only entries | +| Source has emscripten `sourceMappingURL` custom section | `emsymbolizer` source-map path | Single frame per address; no inline expansion (source maps don't carry it) | + +## Tests + +Run the pytest suite under `test-tools/addr2line/tests/`. It builds tiny +purpose-built C/C++ apps, captures their trap sites, and asserts the resolver +behaves correctly under a number of scenarios (single trap, always_inline, +deep inline chain, cross-TU LTO inlining, mid-function trap, multi-frame +call stack, C++ demangling, AOT mode, fast-interp mode, `--no-addr`, +`offset=0`, empty input). Multi-SDK runs (`pytest --multi-sdk`) exercise +the overlay across multiple toolchain versions and require multiple wasi-sdk +installations under `/opt`. + +## See also + +- `test-tools/addr2line/addr2line.py` — the script +- `test-tools/addr2line/tests/` — pytest harness +- `samples/debug-tools/` — minimal end-to-end sample +- `samples/debug-tools-optimized/` — production-realistic workflow (LTO, wasm-opt, strip, AOT, debug companion) diff --git a/test-tools/addr2line/addr2line.py b/test-tools/addr2line/addr2line.py index 800ba37504..810609096c 100644 --- a/test-tools/addr2line/addr2line.py +++ b/test-tools/addr2line/addr2line.py @@ -14,7 +14,7 @@ """ This is a tool to convert addresses, which are from a call-stack dump generated by iwasm, into line info for a wasm file. -When a wasm file is compiled with debug info, it is possible to transfer the address to line info. +When a wasm file is compiled with debug info (-g), it is possible to transfer the address to line info. For example, there is a call-stack dump: @@ -26,27 +26,47 @@ ``` - store the call-stack dump into a file, e.g. call_stack.txt -- run the following command to convert the address into line info: +- run the following command: ``` - $ cd test-tools/addr2line - $ python3 addr2line.py --wasi-sdk --wabt --wasm-file call_stack.txt + $ python3 addr2line.py --wasi-sdk --wabt --wasm-file call_stack.txt ``` - The script will use *wasm-objdump* in wabt to transform address, then use *llvm-dwarfdump* to lookup the line info for each address - in the call-stack dump. -- if addresses are not available in the stack trace (i.e. iwasm <= 1.3.2) or iwasm is used in fast interpreter mode, - run the following command to convert the function index into line info (passing the `--no-addr` option): - ``` - $ python3 addr2line.py --wasi-sdk --wabt --wasm-file call_stack.txt --no-addr - ``` - The script will use *wasm-objdump* in wabt to get the function names corresponding to function indexes, then use *llvm-dwarfdump* to lookup the line info for each - function index in the call-stack dump. + +The script handles two workflows: +- Same-binary: pass the wasm file that ran (must have DWARF debug info) +- Companion: pass a sibling debug companion (.debug.wasm produced by `wasm-opt -Oz -g`). + The production binary that ran can be stripped — the companion has identical code + layout but retains DWARF, so the runtime offsets map correctly. + +Inline expansion (DW_TAG_inlined_subroutine) is automatic — no flag needed. + +By default, offsets are interpreted as coming from classic interpreter mode +(frame_ip has advanced past the trapping opcode). For AOT call stacks, pass +`--mode=aot` so offsets are used verbatim — wamrc commits ip at the start of +each WASM operation, not after. + +For fast-interp call stacks, pass `--mode=fast-interp`. Fast-interp transforms +the bytecode in-memory at load time, so its offsets are meaningless for source +mapping; the script falls back to function-name lookup (same as `--no-addr`). + +If addresses are not available (iwasm <= 1.3.2), pass `--no-addr` to look up +by function index/name only. + +- addr2line.py always applies an interval-table overlay to the outermost + frame's function name. `llvm-symbolizer` unconditionally rewrites that + frame's name from an object-file symbol-table lookup (see + `symbolizeInlinedCode` in + `llvm/lib/DebugInfo/Symbolize/SymbolizableObjectFile.cpp`), and the + symbol-table can return wrong names on wasm. Separately, + `wasm-opt -Oz -g` leaves dead-code-eliminated `DW_TAG_subprogram` DIEs + with `low_pc = 0` behind, violating LLVM's `AddrDieMap` parents-before- + children invariant. The overlay bypasses both by reading DWARF ranges + directly. Inner inlined frames come from `getInliningInfoForAddress` + (a different code path) and are not affected by either mechanism. """ def locate_sourceMappingURL_section(wasm_objdump: Path, wasm_file: Path) -> bool: - """ - Figure out if the wasm file has a sourceMappingURL section. - """ + """Figure out if the wasm file has a sourceMappingURL section.""" cmd = f"{wasm_objdump} -h {wasm_file}" p = subprocess.run( shlex.split(cmd), @@ -55,24 +75,18 @@ def locate_sourceMappingURL_section(wasm_objdump: Path, wasm_file: Path) -> bool text=True, universal_newlines=True, ) - outputs = p.stdout.split(os.linesep) - - for line in outputs: - line = line.strip() - if "sourceMappingURL" in line: + for line in p.stdout.splitlines(): + if "sourceMappingURL" in line.strip(): return True - return False def get_code_section_start(wasm_objdump: Path, wasm_file: Path) -> int: """ - Find the start offset of Code section in a wasm file. + Find the start file offset of the Code section in a wasm file. - if the code section header likes: + Code section header looks like: Code start=0x0000017c end=0x00004382 (size=0x00004206) count: 47 - - the start offset is 0x0000017c """ cmd = f"{wasm_objdump} -h {wasm_file}" p = subprocess.run( @@ -82,155 +96,228 @@ def get_code_section_start(wasm_objdump: Path, wasm_file: Path) -> int: text=True, universal_newlines=True, ) - outputs = p.stdout.split(os.linesep) - - for line in outputs: + for line in p.stdout.splitlines(): line = line.strip() - if "Code" in line: - return int(line.split()[1].split("=")[1], 16) - + if "Code" in line and "start=" in line: + m = re.search(r"start=(0x[0-9a-fA-F]+)", line) + if m: + return int(m.group(1), 16) return -1 -def get_line_info_from_function_addr_dwarf( - dwarf_dump: Path, wasm_file: Path, offset: int -) -> tuple[str, str, str, str]: +def build_subprogram_intervals(dwarf_dump: Path, wasm_file: Path) -> list: """ - Find the location info of a given offset in a wasm file. + Return a list of (low_pc, high_pc, name) for every real callable + DW_TAG_subprogram in the wasm's DWARF. + + Filtered out during parse: + - DIEs with DW_AT_declaration=true (forward declarations; no code). + - DIEs with low_pc == 0. DWARF low_pc is Code-section-relative, and + the section's first byte is the function-count LEB, not any real + function body — so any low_pc=0 DIE is either a declaration or a + dead-code ghost left by wasm-opt (see the "binaryen tombstone" + note in the design doc). + - DIEs with high_pc <= low_pc (empty or degenerate ranges). + + The resulting list is used to overlay the outermost frame's function + name — llvm-symbolizer picks the wrong DW_TAG_subprogram at addresses + where multiple DIEs claim to cover the same PC. See the design doc at + docs/superpowers/specs/2026-07-06-addr2line-collapse-modern-legacy-design.md + for the two independent bug sources. + + Parsed once per run; lookups against the result are O(N) in the + subprogram count, which is small. """ - cmd = f"{dwarf_dump} --lookup={offset} {wasm_file}" - p = subprocess.run( + cmd = f"{dwarf_dump} --debug-info {wasm_file}" + raw = subprocess.run( shlex.split(cmd), check=False, capture_output=True, text=True, universal_newlines=True, - ) - outputs = p.stdout.split(os.linesep) - - function_name, function_file = "", "unknown" - function_line, function_column = "?", "?" - - for line in outputs: - line = line.strip() - - if "DW_AT_name" in line: - function_name = get_dwarf_tag_value("DW_AT_name", line) - - if "DW_AT_decl_file" in line: - function_file = get_dwarf_tag_value("DW_AT_decl_file", line) - - if "Line info" in line: - _, function_line, function_column = parse_line_info(line) - - return (function_name, function_file, function_line, function_column) + ).stdout + + intervals = [] + sp_indent = -1 + low = high = name = None + is_declaration = False + + def _flush(): + # Skip guards: declaration-only, low_pc=0 (wasm-opt DCE ghost or + # invalid), or empty/degenerate range. + if is_declaration: + return + if low is None or low == 0: + return + if high is None or high <= low: + return + if name is None: + return + intervals.append((low, high, name)) + + for raw_line in raw.splitlines(): + # llvm-dwarfdump prefixes DIE-tag lines with their offset + # ("0xNNN:") whose width varies; replace with a fixed sentinel so + # the tag-line indent matches attribute-line indent. + body = re.sub(r"^0x[0-9a-fA-F]+:", " " * 11, raw_line) + stripped = body.lstrip() + if not stripped: + continue + indent = len(body) - len(stripped) + if stripped.startswith("DW_TAG_subprogram"): + _flush() + sp_indent = indent + low = high = name = None + is_declaration = False + continue + if sp_indent < 0: + continue + # A DIE at sp_indent or shallower closes the current subprogram. + if stripped.startswith("DW_TAG_") and indent <= sp_indent: + _flush() + sp_indent = -1 + low = high = name = None + is_declaration = False + continue + # Only attributes one level deeper than the tag belong to the + # subprogram itself; deeper indents are nested DIEs. + if indent != sp_indent + 2: + continue -def get_dwarf_tag_value(tag: str, line: str) -> str: - # Try extracting value as string - STR_PATTERN = rf"{tag}\s+\(\"(.*)\"\)" - m = re.match(STR_PATTERN, line) - if m: - return m.groups()[0] + m = re.match(r"DW_AT_low_pc\s+\(0x([0-9a-fA-F]+)\)", stripped) + if m: + low = int(m.group(1), 16) + continue + m = re.match(r"DW_AT_high_pc\s+\(0x([0-9a-fA-F]+)\)", stripped) + if m: + v = int(m.group(1), 16) + # high_pc is rendered as either an absolute address or — for + # DW_FORM_data* — as size-from-low_pc; pick the larger of the + # two interpretations. + high = v if low is None or v >= low else low + v + continue + m = re.match(r'DW_AT_name\s+\("(.+)"\)', stripped) + if m and name is None: + name = m.group(1) + continue + # DW_AT_declaration(true) → forward declaration; never callable. + if stripped.startswith("DW_AT_declaration") and "true" in stripped: + is_declaration = True - # Try extracting value as integer - INT_PATTERN = rf"{tag}\s+\((\d+)\)" - m = re.match(INT_PATTERN, line) - return m.groups()[0] + _flush() + return intervals -def get_line_info_from_function_name_dwarf( - dwarf_dump: Path, wasm_file: Path, function_name: str -) -> tuple[str, str, str]: - """ - Find the location info of a given function in a wasm file. +def lookup_subprogram_name(intervals: list, dwarf_addr: int) -> str: """ - cmd = f"{dwarf_dump} --name={function_name} {wasm_file}" - p = subprocess.run( - shlex.split(cmd), - check=False, - capture_output=True, - text=True, - universal_newlines=True, - ) - outputs = p.stdout.split(os.linesep) - - function_name, function_file = "", "unknown" - function_line = "?" - - for line in outputs: - line = line.strip() - - if "DW_AT_name" in line: - function_name = get_dwarf_tag_value("DW_AT_name", line) + Return the name of the innermost (smallest-range) DW_TAG_subprogram + whose [low_pc, high_pc) covers `dwarf_addr`, or None. - if "DW_AT_decl_file" in line: - function_file = get_dwarf_tag_value("DW_AT_decl_file", line) - - if "DW_AT_decl_line" in line: - function_line = get_dwarf_tag_value("DW_AT_decl_line", line) - - return (function_name, function_file, function_line) - - -def get_line_info_from_function_addr_sourcemapping( - emsymbolizer: Path, wasm_file: Path, offset: int -) -> tuple[str, str, str, str]: + "Innermost" matters because wasi-libc declarations (low_pc=0, + high_pc spanning much of the binary) overlap real functions; we want + the tightly-bounded one. """ - Find the location info of a given offset in a wasm file which is compiled with emcc. - - {emsymbolizer} {wasm_file} {offset of file} - - there usually are two lines: - ?? - relative path to source file:line:column + best = None + for low, high, name in intervals: + if low <= dwarf_addr < high: + if best is None or (high - low) < (best[1] - best[0]): + best = (low, high, name) + return best[2] if best else None + + +def resolve_address( + symbolizer: Path, wasm_file: Path, dwarf_addr: int, + subprogram_intervals: list, +) -> list: """ - debug_info_source = wasm_file.with_name(f"{wasm_file.name}.map") - cmd = f"{emsymbolizer} -t code -f {debug_info_source} {wasm_file} {offset}" + Resolve one DWARF address to an inline-frame chain, with an + outermost-name overlay from the interval table. + + Runs: -e -f -i 0x + + The symbolizer produces pairs of lines per frame (function name, + then file:line[:column]). With -i, multiple pairs are returned — + innermost inline frame first, outermost real function last. + + The symbolizer's outermost function-name lookup is unreliable on + wasm targets for two independent reasons: + 1. Symbol-table override (LLVM design, not a bug). + `symbolizeInlinedCode` unconditionally rewrites the outermost + frame's FunctionName from `getNameFromSymbolTable` whenever + `FunctionNameKind::LinkageName` and `UseSymbolTable` are both + set — which is the default. Inline frames are provably NOT + rewritten (the override targets only + `getMutableFrame(getNumberOfFrames() - 1)`), so an inline + chain like `do_bad_access → trigger_oob → app_main` from + DWARF gets its LAST entry overwritten by whatever the symbol + table's `getNameFromSymbolTable` returns for that address. + On some wasi-sdk / wasm layouts that returns a wrong function. + 2. `wasm-opt -Oz -g` leaves DIEs for dead-code-eliminated + compiler-rt / libm helpers behind with low_pc=0 and small + high_pc. Those ghost DIEs violate LLVM `AddrDieMap`'s + parents-before-children invariant (child range ⊆ parent + range), so the map's `upper_bound − 1` lookup returns a + ghost DIE for real addresses. This is separate from + mechanism 1 and applies on any clang version. + In both cases only the OUTERMOST frame's function name is wrong; + inline frames and line/column info are correct. See the + design doc at + docs/superpowers/specs/2026-07-06-addr2line-collapse-modern-legacy-design.md + for source-verified references into + llvm/lib/DebugInfo/Symbolize/SymbolizableObjectFile.cpp and + llvm/lib/DebugInfo/DWARF/DWARFUnit.cpp. + + This function unconditionally overlays the outermost frame's + function name with the result of an interval-table lookup that + picks the innermost DW_TAG_subprogram covering `dwarf_addr`. When + the interval table has no covering entry, the symbolizer's + outermost name is left as-is. + + Returns a list of (function_name, file, line, column) tuples. + Each field is a string; line and column may be "?" if unknown. + """ + addr_str = f"0x{dwarf_addr:x}" + cmd = f"{symbolizer} -e {wasm_file} -f -i {addr_str}" p = subprocess.run( shlex.split(cmd), check=False, capture_output=True, text=True, universal_newlines=True, - cwd=Path.cwd(), ) - outputs = p.stdout.split(os.linesep) - - function_name, function_file = "", "unknown" - function_line, function_column = "?", "?" + output_lines = [line for line in p.stdout.splitlines() if line.strip()] - for line in outputs: - line = line.strip() - - if not line: - continue + frames = [] + for i in range(0, len(output_lines) - 1, 2): + func_name = output_lines[i].strip() + location = output_lines[i + 1].strip() - m = re.match(r"(.*):(\d+):(\d+)", line) + # Parse "file:line:column" or "file:line" or "??:0" + m = re.match(r"^(.*):(\d+):(\d+)$", location) if m: - function_file, function_line, function_column = m.groups() - continue + file, line, column = m.group(1), m.group(2), m.group(3) else: - # it's always ??, not sure about that - if "??" != line: - function_name = line + m = re.match(r"^(.*):(\d+)$", location) + if m: + file, line, column = m.group(1), m.group(2), "?" + else: + file, line, column = location, "?", "?" - return (function_name, function_file, function_line, function_column) + frames.append((func_name, file, line, column)) + # Overlay the outermost frame's name from the interval table when + # available. Inner frames are left alone. + if frames: + name = lookup_subprogram_name(subprogram_intervals, dwarf_addr) + if name: + _, file, line, column = frames[-1] + frames[-1] = (name, file, line, column) -def parse_line_info(line_info: str) -> tuple[str, str, str]: - """ - line_info -> [file, line, column] - """ - PATTERN = r"Line info: file \'(.+)\', line ([0-9]+), column ([0-9]+)" - m = re.search(PATTERN, line_info) - assert m is not None + return frames - file, line, column = m.groups() - return (file, int(line), int(column)) - -def parse_call_stack_line(line: str) -> tuple[str, str, str]: +def parse_call_stack_line(line: str) -> tuple: """ New format (WAMR > 1.3.2): #00: 0x0a04 - $f18 => (00, 0x0a04, $f18) @@ -241,7 +328,6 @@ def parse_call_stack_line(line: str) -> tuple[str, str, str]: _start (always): #05: 0x011f - _start => (05, 0x011f, _start) """ - # New format and Text format and _start PATTERN = r"#([0-9]+): 0x([0-9a-f]+) - (\S+)" m = re.match(PATTERN, line) @@ -257,7 +343,8 @@ def parse_call_stack_line(line: str) -> tuple[str, str, str]: return None -def parse_module_functions(wasm_objdump: Path, wasm_file: Path) -> dict[str, str]: +def parse_module_functions(wasm_objdump: Path, wasm_file: Path) -> dict: + """Map function index to name from wasm-objdump output.""" function_index_to_name = {} cmd = f"{wasm_objdump} -x {wasm_file} --section=function" @@ -268,16 +355,13 @@ def parse_module_functions(wasm_objdump: Path, wasm_file: Path) -> dict[str, str text=True, universal_newlines=True, ) - outputs = p.stdout.split(os.linesep) - - for line in outputs: - if not f"func[" in line: + for line in p.stdout.splitlines(): + if "func[" not in line: continue - PATTERN = r".*func\[([0-9]+)\].*<(.*)>" m = re.match(PATTERN, line) - assert m is not None - + if m is None: + continue index = m.groups()[0] name = m.groups()[1] function_index_to_name[index] = name @@ -285,6 +369,121 @@ def parse_module_functions(wasm_objdump: Path, wasm_file: Path) -> dict[str, str return function_index_to_name +def get_line_info_from_function_name_dwarf( + dwarf_dump: Path, wasm_file: Path, function_name: str +) -> tuple: + """ + Used by --no-addr mode (fast-interp call stacks, or WAMR <= 1.3.2 + output where addresses are absent). + + Resolves a function name to its declaration's (file, decl_line) by + parsing `llvm-dwarfdump --name=` output: collect every + matching DW_TAG_subprogram block, then prefer a non-sysroot match + over a wasi-libc declaration of the same name (short names like + `b`, `c` frequently collide with wasi-libc helpers). + + TODO: limitation — `llvm-symbolizer` is address-driven and has no + name-to-source mode, so this helper manually parses llvm-dwarfdump + text output. The resolved location is the function's declaration + line, not the call site. A robust replacement would need a + programmatic DWARF reader (e.g. pyelftools) or a future LLVM CLI + exposing name-keyed lookup. + + Returns (function_name, file, line). + """ + cmd = f"{dwarf_dump} --name={function_name} {wasm_file}" + p = subprocess.run( + shlex.split(cmd), + check=False, + capture_output=True, + text=True, + universal_newlines=True, + ) + + # Collect every (name, file, line) triple from the output. Each + # DW_TAG_subprogram block is separated by a blank line. + candidates = [] + cur = {} + for line in p.stdout.splitlines() + [""]: + line = line.strip() + if not line: + if cur: + candidates.append(( + cur.get("name", ""), + cur.get("file", "unknown"), + cur.get("line", "?"), + )) + cur = {} + continue + m = re.match(r'DW_AT_name\s+\("(.+)"\)', line) + if m: + cur["name"] = m.group(1) + continue + m = re.match(r'DW_AT_decl_file\s+\("(.+)"\)', line) + if m: + cur["file"] = m.group(1) + continue + m = re.match(r"DW_AT_decl_line\s+\((\d+)\)", line) + if m: + cur["line"] = m.group(1) + + # Prefer a candidate whose source file isn't from wasi-sysroot / + # wasi-libc / wasi-sdk — short names like `b`, `c` collide with + # internal libc helpers and llvm-dwarfdump's name lookup returns + # every match across the binary. + def _is_sysroot(p): + return "wasi-sysroot" in p or "wasi-libc" in p or p.startswith("wasisdk://") + + for name, file, line in candidates: + if name == function_name and not _is_sysroot(file): + return (name, file, line) + # Fall back to the last candidate (preserves prior behavior when + # all matches are sysroot-ish or the name doesn't match exactly). + if candidates: + return candidates[-1] + return ("", "unknown", "?") + + +def get_line_info_from_function_addr_sourcemapping( + emsymbolizer: Path, wasm_file: Path, offset: int +) -> tuple: + """ + Find the location info of a given offset in a wasm file compiled with emcc. + + {emsymbolizer} {wasm_file} {offset} + + Output is two lines: + ?? + relative path to source:line:column + """ + debug_info_source = wasm_file.with_name(f"{wasm_file.name}.map") + cmd = f"{emsymbolizer} -t code -f {debug_info_source} {wasm_file} {offset}" + p = subprocess.run( + shlex.split(cmd), + check=False, + capture_output=True, + text=True, + universal_newlines=True, + cwd=Path.cwd(), + ) + + function_name, function_file = "", "unknown" + function_line, function_column = "?", "?" + + for line in p.stdout.splitlines(): + line = line.strip() + if not line: + continue + m = re.match(r"(.*):(\d+):(\d+)", line) + if m: + function_file, function_line, function_column = m.groups() + continue + if "??" != line: + function_name = line + + return (function_name, function_file, function_line, function_column) + + def demangle(cxxfilt: Path, function_name: str) -> str: cmd = f"{cxxfilt} -n {function_name}" p = subprocess.run( @@ -297,45 +496,133 @@ def demangle(cxxfilt: Path, function_name: str) -> str: return p.stdout.strip() +def print_frames(index_str: str, frames: list, cxxfilt: Path) -> None: + """ + Print resolved frames with consistent indentation. Inlined functions + (frames before the outermost) get an "(inlined into )" suffix + so the chain relationship is unambiguous. + + Single frame (no inlines): + 0: c + at trap.c:4:5 + + Multiple frames (inline chain — innermost first): + 0: do_bad_access (inlined into trigger_oob) + at oob_access.c:11:5 + trigger_oob (inlined into app_main) + at oob_main.c:17:5 + app_main + at oob_main.c:23:5 + + The first frame uses ":" prefix; subsequent frames use the same + width of leading spaces for vertical alignment. + """ + prefix = f"{index_str}:" + indent = " " * len(prefix) + + demangled_names = [ + demangle(cxxfilt, fn) if fn != "??" else "" + for fn, _, _, _ in frames + ] + + for i, (func_name, file, line, column) in enumerate(frames): + leading = prefix if i == 0 else indent + # All frames except the outermost are inlined into the next one in the chain. + if i < len(frames) - 1: + suffix = f" (inlined into {demangled_names[i + 1]})" + else: + suffix = "" + print(f"{leading} {demangled_names[i]}{suffix}") + # Don't print "??:0:0" — collapse to "??:0". + if file == "??" and line in ("0", "?"): + print("\tat ??:0") + elif column != "?": + print(f"\tat {file}:{line}:{column}") + else: + print(f"\tat {file}:{line}") + + def main(): parser = argparse.ArgumentParser(description="addr2line for wasm") - parser.add_argument("--wasi-sdk", type=Path, help="path to wasi-sdk") - parser.add_argument("--wabt", type=Path, help="path to wabt") - parser.add_argument("--wasm-file", type=Path, help="path to wasm file") + parser.add_argument("--wasi-sdk", type=Path, required=True, help="path to wasi-sdk") + parser.add_argument("--wabt", type=Path, required=True, help="path to wabt") + parser.add_argument("--wasm-file", type=Path, required=True, help="path to wasm file (must have DWARF)") parser.add_argument("call_stack_file", type=Path, help="path to a call stack file") parser.add_argument( "--no-addr", action="store_true", help="use call stack without addresses or from fast interpreter mode", ) + parser.add_argument( + "--mode", + choices=["interp", "aot", "fast-interp"], + default="interp", + help=( + "WAMR execution mode that produced the call stack. " + "interp: classic interpreter, frame_ip is post-advance, subtract 1 " + "from offsets to land on the trapping opcode. " + "aot: wamrc commits ip at the start of each WASM operation, use " + "offsets verbatim. " + "fast-interp: offsets are relative to a transformed in-memory " + "bytecode and cannot be mapped to source line — falls back to " + "function-name lookup (same as --no-addr). " + "Default: interp." + ), + ) parser.add_argument("--emsdk", type=Path, help="path to emsdk") args = parser.parse_args() + # fast-interp offsets aren't mappable; treat as --no-addr + if args.mode == "fast-interp": + args.no_addr = True + wasm_objdump = args.wabt.joinpath("bin/wasm-objdump") - assert wasm_objdump.exists() + assert wasm_objdump.exists(), f"wasm-objdump not found at {wasm_objdump}" + + # llvm-symbolizer handles address->source resolution and inline-frame + # expansion via `-f -i`. Ships in wasi-sdk 29+, which is the project's + # minimum supported wasi-sdk for the debug-tools samples. + llvm_symbolizer = args.wasi_sdk.joinpath("bin/llvm-symbolizer") + assert llvm_symbolizer.exists(), ( + f"llvm-symbolizer not found at {llvm_symbolizer}; " + f"need wasi-sdk 29 or newer" + ) llvm_dwarf_dump = args.wasi_sdk.joinpath("bin/llvm-dwarfdump") - assert llvm_dwarf_dump.exists() + assert llvm_dwarf_dump.exists(), f"llvm-dwarfdump not found at {llvm_dwarf_dump}" llvm_cxxfilt = args.wasi_sdk.joinpath("bin/llvm-cxxfilt") - assert llvm_cxxfilt.exists() + assert llvm_cxxfilt.exists(), f"llvm-cxxfilt not found at {llvm_cxxfilt}" emcc_production = locate_sourceMappingURL_section(wasm_objdump, args.wasm_file) if emcc_production: if args.emsdk is None: print("Please provide the path to emsdk via --emsdk") return -1 - emsymbolizer = args.emsdk.joinpath("upstream/emscripten/emsymbolizer") assert emsymbolizer.exists() code_section_start = get_code_section_start(wasm_objdump, args.wasm_file) if code_section_start == -1: + print("Could not find Code section in wasm file", file=sys.stderr) return -1 function_index_to_name = parse_module_functions(wasm_objdump, args.wasm_file) - assert args.call_stack_file.exists() + # Build the DW_TAG_subprogram interval table once at startup. The + # per-frame resolve_address() call overlays the outermost function + # name from this table to fix LLVM's mis-reports on wasm: the + # symbolizer applies an unconditional symbol-table override to the + # outermost frame (see SymbolizableObjectFile.cpp), and wasm-opt + # DCE ghosts violate AddrDieMap's invariant (see DWARFUnit.cpp). + # Empty when we're on the emscripten sourceMappingURL path, which + # doesn't use DWARF. + subprogram_intervals = ( + [] if emcc_production + else build_subprogram_intervals(llvm_dwarf_dump, args.wasm_file) + ) + + assert args.call_stack_file.exists(), f"call stack file not found: {args.call_stack_file}" with open(args.call_stack_file, "rt", encoding="ascii") as f: for i, line in enumerate(f): line = line.strip() @@ -348,63 +635,121 @@ def main(): continue _, offset, index = splitted + index_str = str(i) + if args.no_addr: # FIXME: w/ emcc production if not index.startswith("$f"): # E.g. _start or Text format - print(f"{i}: {index}") + print(f"{index_str}: {index}") continue - index = index[2:] + func_idx = index[2:] - if index not in function_index_to_name: - print(f"{i}: {line}") + if func_idx not in function_index_to_name: + print(f"{index_str}: {line}") continue if not emcc_production: _, function_file, function_line = ( get_line_info_from_function_name_dwarf( - llvm_dwarf_dump, - args.wasm_file, - function_index_to_name[index], + llvm_dwarf_dump, args.wasm_file, + function_index_to_name[func_idx], ) ) else: - _, function_file, function_line = _, "unknown", "?" + function_file, function_line = "unknown", "?" - function_name = demangle(llvm_cxxfilt, function_index_to_name[index]) - print(f"{i}: {function_name}") + function_name = demangle(llvm_cxxfilt, function_index_to_name[func_idx]) + print(f"{index_str}: {function_name}") print(f"\tat {function_file}:{function_line}") - else: - offset = int(offset, 16) - # match the algorithm in wasm_interp_create_call_stack() - # either a *offset* to *code* section start - # or a *offset* in a file - assert offset > code_section_start - offset = offset - code_section_start - - if emcc_production: - function_name, function_file, function_line, function_column = ( - get_line_info_from_function_addr_sourcemapping( - emsymbolizer, args.wasm_file, offset + continue + + # Address-based lookup + offset_int = int(offset, 16) + + # WAMR reports offset 0 when frame_ip wasn't captured (e.g., trap + # at function entry, or top frame of an iwasm -f invocation where + # ip wasn't sync'd before the trap). Fall back to function-name + # lookup for the function header instead of asserting. + if offset_int == 0 or offset_int <= code_section_start: + if not emcc_production: + # Resolve function name from index ($fN) or use the name directly + if index.startswith("$f"): + func_idx = index[2:] + func_name = function_index_to_name.get(func_idx, index) + else: + func_name = index + _, function_file, function_line = ( + get_line_info_from_function_name_dwarf( + llvm_dwarf_dump, args.wasm_file, func_name ) ) - else: - function_name, function_file, function_line, function_column = ( - get_line_info_from_function_addr_dwarf( - llvm_dwarf_dump, args.wasm_file, offset - ) + print(f"{index_str}: {demangle(llvm_cxxfilt, func_name)}") + print(f"\tat {function_file}:{function_line} " + f"(offset=0 — function entry, no inline info)") + continue + # emcc path: just print raw + print(f"{index_str}: {index}") + print(f"\tat unknown:?:? (offset=0 — frame_ip not captured)") + continue + + # Mode-dependent offset adjustment: + # - interp: subtract 1 because frame_ip has advanced past the trapping + # opcode before the trap handler runs (FETCH_OPCODE_AND_DISPATCH + # increments ip before dispatching, plus LEB reads inside handlers). + # - aot: use offset verbatim. wamrc commits ip at the start of each + # WASM operation, before any LEB reads — the offset is already at + # the opcode boundary. + # (fast-interp is handled above by forcing --no-addr; we never reach + # this branch for it.) + adjustment = 1 if args.mode == "interp" else 0 + dwarf_addr = offset_int - code_section_start - adjustment + + if emcc_production: + # Source-map path doesn't support inline expansion; emit single frame. + func_name, file, line, column = ( + get_line_info_from_function_addr_sourcemapping( + emsymbolizer, args.wasm_file, dwarf_addr ) + ) + # Fall back to function index name if unresolved + if func_name == "" and index.startswith("$f"): + func_name = function_index_to_name.get(index[2:], index) + func_name = demangle(llvm_cxxfilt, func_name) + print(f"{index_str}: {func_name}") + print(f"\tat {file}:{line}:{column}") + continue - # if can't parse function_name, use name section or - if function_name == "": - if index.startswith("$f"): - function_name = function_index_to_name.get(index[2:], index) - else: - function_name = index + frames = resolve_address( + llvm_symbolizer, args.wasm_file, dwarf_addr, + subprogram_intervals, + ) - function_name = demangle(llvm_cxxfilt, function_name) + if not frames: + # Fall back to function index name + if index.startswith("$f"): + func_name = function_index_to_name.get(index[2:], index) + else: + func_name = index + print(f"{index_str}: {demangle(llvm_cxxfilt, func_name)}") + print(f"\tat unknown:?:?") + continue - print(f"{i}: {function_name}") - print(f"\tat {function_file}:{function_line}:{function_column}") + # Last-resort fallback for the outermost frame: when neither + # llvm-symbolizer nor the dwarfdump subprogram-name overlay + # produced a clean name (e.g. the address is in a function + # whose DWARF has no PC range — declaration-only + # DW_TAG_subprogram), substitute the function index -> name + # from wasm-objdump. + if index.startswith("$f"): + expected = function_index_to_name.get(index[2:]) + actual = frames[-1][0] + if expected and ( + actual == "??" + or (actual != expected and frames[-1][1] in ("??", "unknown")) + ): + frames[-1] = (expected,) + frames[-1][1:] + + print_frames(index_str, frames, llvm_cxxfilt) return 0 diff --git a/test-tools/addr2line/tests/README.md b/test-tools/addr2line/tests/README.md new file mode 100644 index 0000000000..74a35b7532 --- /dev/null +++ b/test-tools/addr2line/tests/README.md @@ -0,0 +1,140 @@ +# addr2line.py tests + +Pytest suite for `test-tools/addr2line/addr2line.py`. Replaces the older +diagnostic harness that lived under `samples/debug-tools/llvm-bug-experiment/`. + +## What's covered + +Each test case exercises one specific aspect of `addr2line.py`: + +| Test | Source | Purpose | +|------|--------|---------| +| `test_build_subprogram_intervals_filters_invalid_dies` | `apps/simple.c` (+ wasm-opt post-process) | Builds a wasi-libc-linked wasm and post-processes with `wasm-opt -Oz -g` to trigger the DCE-ghost pattern. Asserts the resulting interval table contains no `low_pc = 0` entries and no `high_pc <= low_pc` entries. Guards the skip-guard invariant in `build_subprogram_intervals`. | +| `test_simple_resolution` | `apps/simple.c` | Baseline. One function, one trap. Asserts the resolved frame names the function and points at its source file — proves the basic address → `(func, file:line)` plumbing works end to end. | +| `test_inline_chain_basic` | `apps/always_inline.c` | A trap inside an `__attribute__((always_inline))` helper produces a 2-frame inline chain (the helper plus its caller). Asserts addr2line emits BOTH names and the `(inlined into )` annotation that ties them together — guards against future regressions in `print_frames`. | +| `test_inline_chain_deep` | `apps/deep_inline_chain.c` | Four nested always-inline functions all collapse into one WASM function under `-O0 -g`. Asserts all four names render in the inline chain AND that the outermost frame name is `app_main` (not a wasi-libc symbol like `__multi3` that overlaps app_main's PC range). This is the test that catches the "(inlined into free)" / wasi-libc-shadow class of legacy-resolver regressions. | +| `test_cross_tu_inline` | `apps/multi_file_recur_*.c` | The canonical trigger for both LLVM outermost-name failure modes: two TUs, `-Oz -flto`, then `wasm-opt -Oz -g` (which produces DCE ghosts). Both the symbol-table override and the AddrDieMap invariant violation apply here, so the test only pins the limited contract that the *line table* stays inside our source files (or returns `??:0`) — it must NEVER reach into wasi-libc / wasi-sysroot. | +| `test_trap_mid_function` | `apps/trap_in_loop.c` | Trap is inside a loop body, several instructions past function entry. Asserts the resolved line is > 10 (i.e. NOT the function declaration line). Catches future regressions where addr2line might collapse mid-function addresses back to function-entry. | +| `test_multi_frame_callstack` | `apps/multi_frame.c` | Four distinct WASM functions in a call chain. Asserts every frame renders with the correct function name and a distinct frame index — catches frame-numbering or per-frame-resolution bugs that a single-frame test misses. | +| `test_cxx_demangling` | `apps/cxx_mangled.cpp` | A templated, namespaced C++ function exercises the cxxfilt pass. Asserts mangled `_Z...` never leaks to the user and that the line table resolves to our `.cpp` (not wasi-libc). Doesn't pin the symbolizer's function-name choice — both clang < 22 and clang 22+ pick the wrong subprogram on some C++ template addresses on wasm. | +| `test_aot_mode` | `apps/simple.c` (`-Oz -g`) | `--mode=aot` uses no `-1` adjustment (wamrc commits ip at instruction-start, not post-advance). The test picks an address at `low_pc+delta` and asserts the function still resolves under `--mode=aot`. Skips when `crash()` gets inlined away under `-Oz`. | +| `test_fast_interp_mode` | `fixtures/fast_interp_stack.txt` + `apps/simple.c` | The fixture has `$f0`/`$f1` indices; `--mode=fast-interp` forces function-name lookup (offsets aren't mappable). Asserts both indices resolve to the real wasm name-section entries (`crash`/`app_main`). | +| `test_no_addr_mode` | `fixtures/fast_interp_stack.txt` + `apps/simple.c` | Same fixture; `--no-addr` is the explicit form of function-name lookup. Additionally asserts the DWARF decl-file (`simple.c`) is surfaced (function-name lookup falls back to declaration-line, not call-site). | +| `test_offset_zero_fallback` | `apps/simple.c` (inline cs) | WAMR reports `offset=0` when `frame_ip` wasn't captured (trap at function entry, top frame of `iwasm -f`, etc.). The test asserts addr2line falls back to function-name resolution gracefully and prints the `app_main` name — does NOT crash or assert. | +| `test_empty_input` | `fixtures/empty_stack.txt` + `apps/simple.c` | A zero-line call stack should exit cleanly (rc=0) and produce no output. Defends against an `IndexError`-style crash when the loop body never runs. | + +Sources under `apps/` are written for the test suite, not copied from +`samples/`. They're small (~10–25 lines each) and target specific edge +cases that aren't covered by the sample-CI integration tests. + +## Running + +```bash +# Default: against $WASI_SDK_PATH or /opt/wasi-sdk +./run_tests.sh +# or +python3 -m pytest -v + +# Fixture-only (fast, no builds) +./run_tests.sh -m "not slow" + +# Multi-SDK: parametrize build-based tests over every detected +# /opt/wasi-sdk-*-x86_64-linux installation. Run each build-based +# test once per detected wasi-sdk. +./run_tests.sh --multi-sdk + +# Verbose: print each test's addr2line input + stdout/stderr so you +# can see what got resolved. Use -v -s to surface it live; -v alone +# captures it and only shows it on failure. +./run_tests.sh -v -s +``` + +## Background: why the interval-table overlay exists + +`addr2line.py` exists primarily to convert WAMR call-stack dumps into +source `file:line:column`. On most well-behaved DWARF inputs this is +a thin wrapper around `llvm-symbolizer -f -i`. But on wasm targets, +the symbolizer's OUTERMOST frame's function name can be wrong for +two independent reasons — both source-verified against upstream +LLVM (`SymbolizableObjectFile.cpp` and `DWARFUnit.cpp`): + +1. **Symbol-table override on the outermost frame** (LLVM design + choice). `symbolizeInlinedCode` unconditionally rewrites the + outermost frame's `FunctionName` from `getNameFromSymbolTable` + whenever `FunctionNameKind::LinkageName` and `UseSymbolTable` are + both set. Inline frames are provably NOT rewritten — the override + is applied only to `getMutableFrame(getNumberOfFrames() - 1)`. On + some wasi-sdk builds the symbol-table lookup returns the wrong + function for an address that DWARF places correctly. +2. **`AddrDieMap` invariant violation** (root cause in binaryen's + DWARF preservation model). `wasm-opt -Oz -g` deletes functions + from the wasm module but its DWARF updater does not remove the + corresponding `DW_TAG_subprogram` DIEs — instead, when an old + address can no longer be mapped to a surviving IR node, + `LocationUpdater::getNewFuncStart` in `src/wasm/wasm-debug.cpp` + returns `0` as a tombstone value and `updateDIE` writes it into + `DW_AT_low_pc`. The `DW_TAG_subprogram` remains in place; sometimes + its `DW_AT_high_pc` retains its original value. Result: stale + "ghost" DIEs like `fmaf [0, 0x72)`. LLVM's `updateAddressDieMap` + in `DWARFUnit.cpp` assumes children are inserted after parents + and each child range ⊆ parent range; these ghosts violate that + invariant, so the map's `upper_bound − 1` lookup returns an + insertion-order-dependent DIE for real addresses. + +The symptom is the same in both cases — only the OUTERMOST frame's +function name is wrong. Line/column info and inner inlined frame +names are correct because they come from a different LLVM code path +(`getInliningInfoForAddress`, which walks `getParent()` links from +the leaf DIE and does NOT go through the symbol-table override). + +`addr2line.py` always runs `llvm-dwarfdump --debug-info` at startup, +builds a `(low_pc, high_pc, name)` interval table for every real +callable `DW_TAG_subprogram` (three skip guards described below), and +overlays the outermost frame's name from an innermost-wins lookup on +that table. + +### Skip guards applied to the interval table + +- `DW_AT_declaration=true` → forward declaration only, no code. +- `low_pc == 0` → structurally impossible for real code (wasm binary + format guarantees the Code section starts after Type + Function + section headers). Any `low_pc = 0` DIE is a wasm-opt DCE ghost or + a broken producer's output. +- `high_pc <= low_pc` → empty or degenerate range. Never observed + in practice; kept as a defense-in-depth guard against future + toolchain changes. + +Multi-SDK CI (`./run_tests.sh --multi-sdk`) exercises the parser +against DWARF produced by wasi-sdk 29 (clang 21) and wasi-sdk 33 +(clang 22) to catch DIE-encoding differences between clang versions. + +## Why we left samples/debug-tools/llvm-bug-experiment behind + +The earlier experiment was useful for diagnosing the bug but wasn't a +proper test suite — it was a one-shot script that printed status to +stdout. This pytest suite covers more cases, runs in CI per-PR (default +SDK) and nightly (multi-SDK), and is decoupled from sample evolution. + +## Layout + +``` +test-tools/addr2line/tests/ +├── README.md # this file +├── conftest.py # fixtures (wasi_sdk, build_wasm, run_addr2line, ...) +├── test_addr2line.py # the tests +├── pytest.ini # marker definitions +├── run_tests.sh # convenience wrapper +├── apps/ # purpose-built test sources +│ ├── simple.c +│ ├── always_inline.c +│ ├── deep_inline_chain.c +│ ├── multi_file_recur_main.c +│ ├── multi_file_recur.c +│ ├── trap_in_loop.c +│ ├── multi_frame.c +│ └── cxx_mangled.cpp +└── fixtures/ # plaintext call-stack inputs + ├── fast_interp_stack.txt + ├── aot_stack.txt + └── empty_stack.txt +``` diff --git a/test-tools/addr2line/tests/apps/always_inline.c b/test-tools/addr2line/tests/apps/always_inline.c new file mode 100644 index 0000000000..4be1c6eaed --- /dev/null +++ b/test-tools/addr2line/tests/apps/always_inline.c @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2026 Intel Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ + +/* Forced-inline helper containing the trap. The trap address falls + inside the inlined region, so addr2line.py must report both + `inner` and `outer` under a single runtime frame, with `inner` + carrying the `(inlined into outer)` suffix. */ + +__attribute__((always_inline)) static inline void +inner(void) +{ + __builtin_trap(); +} + +void +outer(void) +{ + inner(); +} + +void +app_main(void) +{ + outer(); +} diff --git a/test-tools/addr2line/tests/apps/cxx_mangled.cpp b/test-tools/addr2line/tests/apps/cxx_mangled.cpp new file mode 100644 index 0000000000..29fafedf57 --- /dev/null +++ b/test-tools/addr2line/tests/apps/cxx_mangled.cpp @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2026 Intel Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ + +/* C++ source with a templated and namespaced function. addr2line.py + should pass the mangled symbol through llvm-cxxfilt and produce a + readable demangled name in the output. */ + +namespace app { +namespace ns { + +template +__attribute__((noinline)) static void +crash_with(T) +{ + __builtin_trap(); +} + +class Worker +{ + public: + __attribute__((noinline)) void run() { crash_with(42); } +}; + +} // namespace ns +} // namespace app + +extern "C" void +app_main(void) +{ + app::ns::Worker w; + w.run(); +} diff --git a/test-tools/addr2line/tests/apps/deep_inline_chain.c b/test-tools/addr2line/tests/apps/deep_inline_chain.c new file mode 100644 index 0000000000..b392a13bc5 --- /dev/null +++ b/test-tools/addr2line/tests/apps/deep_inline_chain.c @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2026 Intel Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ + +/* Four nested always_inline helpers. The trap inside level4 must + produce a 4-deep inline chain in the output, with consistent + indentation across all four frames. */ + +__attribute__((always_inline)) static inline void +level4(void) +{ + __builtin_trap(); +} +__attribute__((always_inline)) static inline void +level3(void) +{ + level4(); +} +__attribute__((always_inline)) static inline void +level2(void) +{ + level3(); +} +__attribute__((always_inline)) static inline void +level1(void) +{ + level2(); +} + +void +app_main(void) +{ + level1(); +} diff --git a/test-tools/addr2line/tests/apps/multi_file_recur.c b/test-tools/addr2line/tests/apps/multi_file_recur.c new file mode 100644 index 0000000000..c63ca8f25e --- /dev/null +++ b/test-tools/addr2line/tests/apps/multi_file_recur.c @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2026 Intel Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ + +static volatile int sink; + +int +recur(int depth) +{ + /* Allocate stack space and use it so the call isn't trivially + optimized away. Trap inside the loop body. */ + volatile char buf[64]; + buf[0] = (char)depth; + buf[63] = (char)(depth >> 8); + sink = buf[0] + buf[63]; + if (depth == 100) { + __builtin_trap(); + } + /* Use the return value after the call to prevent tail-call + conversion to a loop under -Oz -flto. */ + int r = recur(depth + 1); + return r + buf[0]; +} diff --git a/test-tools/addr2line/tests/apps/multi_file_recur_main.c b/test-tools/addr2line/tests/apps/multi_file_recur_main.c new file mode 100644 index 0000000000..b24b355ce7 --- /dev/null +++ b/test-tools/addr2line/tests/apps/multi_file_recur_main.c @@ -0,0 +1,18 @@ +/* + * Copyright (C) 2026 Intel Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ + +/* Multi-file recursion: app_main calls into recur() defined in another + TU. Compiled with -Oz -g -flto, the linker can inline cross-TU and the + trap address resolves to a chain that crosses files. This is the + canonical case that triggers the LLVM symbolizer wasm bug on clang < 22. */ + +int +recur(int depth); + +void +app_main(void) +{ + recur(0); +} diff --git a/test-tools/addr2line/tests/apps/multi_frame.c b/test-tools/addr2line/tests/apps/multi_frame.c new file mode 100644 index 0000000000..144fa40664 --- /dev/null +++ b/test-tools/addr2line/tests/apps/multi_frame.c @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2026 Intel Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ + +/* Three noinline functions producing a 3-frame WASM call stack at the + trap (plus app_main). addr2line.py must report all four frames with + distinct indices. */ + +__attribute__((noinline)) void +bot(void) +{ + __builtin_trap(); +} + +__attribute__((noinline)) void +mid(void) +{ + bot(); +} + +__attribute__((noinline)) void +top(void) +{ + mid(); +} + +void +app_main(void) +{ + top(); +} diff --git a/test-tools/addr2line/tests/apps/simple.c b/test-tools/addr2line/tests/apps/simple.c new file mode 100644 index 0000000000..5a2d0eeb6e --- /dev/null +++ b/test-tools/addr2line/tests/apps/simple.c @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2026 Intel Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ + +/* Baseline test: single function, single trap. The address of + __builtin_trap() must symbolicate back to file=simple.c, function=crash. */ + +void +crash(void) +{ + __builtin_trap(); +} + +void +app_main(void) +{ + crash(); +} diff --git a/test-tools/addr2line/tests/apps/trap_in_loop.c b/test-tools/addr2line/tests/apps/trap_in_loop.c new file mode 100644 index 0000000000..15546da174 --- /dev/null +++ b/test-tools/addr2line/tests/apps/trap_in_loop.c @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2026 Intel Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ + +/* Trap inside a for-loop body. The trap address is neither at function + entry nor at function exit; it's mid-function. addr2line.py must + resolve it to the trap line, not the function declaration line. */ + +static volatile int sink; + +void +crash(void) +{ + for (int i = 0; i < 10; i++) { + sink = i; + if (i == 7) { + __builtin_trap(); + } + } +} + +void +app_main(void) +{ + crash(); +} diff --git a/test-tools/addr2line/tests/conftest.py b/test-tools/addr2line/tests/conftest.py new file mode 100644 index 0000000000..242e321181 --- /dev/null +++ b/test-tools/addr2line/tests/conftest.py @@ -0,0 +1,277 @@ +# Copyright (C) 2026 Intel Corporation. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +""" +pytest fixtures for the addr2line.py test suite. + +Default mode: a single wasi-sdk (from $WASI_SDK_PATH or /opt/wasi-sdk). +With --multi-sdk: discover all /opt/wasi-sdk-*-x86_64-linux installations +and parametrize the wasi_sdk fixture so build-based tests run once per SDK. +""" + +import os +import shlex +import subprocess +import sys +from pathlib import Path + +import pytest + + +# --- CLI options ---------------------------------------------------------- + +def pytest_addoption(parser): + parser.addoption( + "--multi-sdk", + action="store_true", + default=False, + help="Run build-based tests against every detected " + "/opt/wasi-sdk-*-x86_64-linux installation (default: only " + "the SDK from WASI_SDK_PATH or /opt/wasi-sdk).", + ) + + +# --- Tool-discovery helpers ---------------------------------------------- + +def _detect_sdks_under_opt(): + """Return a sorted list of (version_str, Path) tuples for every + wasi-sdk under /opt that has a working clang AND ships + llvm-symbolizer. addr2line.py requires wasi-sdk 29+; pre-29 + bundles omit llvm-symbolizer and are filtered out here. Newest + version last.""" + found = [] + for d in sorted(Path("/opt").glob("wasi-sdk-*-x86_64-linux")): + if not (d / "bin" / "clang").exists(): + continue + if not (d / "bin" / "llvm-symbolizer").exists(): + continue + ver = d.name.replace("wasi-sdk-", "").replace("-x86_64-linux", "") + found.append((ver, d)) + return found + + +def _default_sdk(): + """Return the single SDK to use when --multi-sdk is not given.""" + env = os.environ.get("WASI_SDK_PATH") + if env and (Path(env) / "bin" / "clang").exists(): + return Path(env) + default = Path("/opt/wasi-sdk") + if (default / "bin" / "clang").exists(): + return default + return None + + +# --- Parametrization ------------------------------------------------------ + +def pytest_generate_tests(metafunc): + """Parametrize the wasi_sdk fixture based on --multi-sdk.""" + if "wasi_sdk" not in metafunc.fixturenames: + return + + multi = metafunc.config.getoption("--multi-sdk") + if multi: + sdks = _detect_sdks_under_opt() + if not sdks: + pytest.skip("--multi-sdk: no /opt/wasi-sdk-*-x86_64-linux found") + ids = [ver for ver, _ in sdks] + paths = [path for _, path in sdks] + metafunc.parametrize("wasi_sdk", paths, ids=ids, indirect=False) + else: + sdk = _default_sdk() + if sdk is None: + pytest.skip("no wasi-sdk found (set WASI_SDK_PATH or install /opt/wasi-sdk)") + metafunc.parametrize("wasi_sdk", [sdk], ids=[sdk.name], indirect=False) + + +# --- Session-scoped path fixtures ---------------------------------------- + +@pytest.fixture(scope="session") +def wabt(): + p = Path(os.environ.get("WABT_PATH", "/opt/wabt")) + if not (p / "bin" / "wasm-objdump").exists(): + pytest.skip(f"wabt not found at {p}") + return p + + +@pytest.fixture(scope="session") +def binaryen(): + p = Path(os.environ.get("BINARYEN_PATH", "/opt/binaryen")) + if not (p / "bin" / "wasm-opt").exists(): + pytest.skip(f"binaryen not found at {p}") + return p + + +@pytest.fixture(scope="session") +def addr2line_script(): + """Return the absolute path to addr2line.py.""" + here = Path(__file__).resolve().parent + return here.parent / "addr2line.py" + + +@pytest.fixture(scope="session") +def apps_dir(): + return Path(__file__).resolve().parent / "apps" + + +@pytest.fixture(scope="session") +def fixtures_dir(): + return Path(__file__).resolve().parent / "fixtures" + + +# --- Build / invoke fixtures --------------------------------------------- + +@pytest.fixture +def build_wasm(wasi_sdk, apps_dir, tmp_path_factory): + """Build wasm from sources under apps/ with the given flags. + + Cached per (sources tuple, flags tuple, language) within this test + session and SDK. + """ + cache = {} + + def _build(sources, flags, language="c"): + key = (tuple(sources), tuple(flags), language, str(wasi_sdk)) + if key in cache: + return cache[key] + + compiler = wasi_sdk / "bin" / ("clang++" if language == "cxx" else "clang") + outdir = tmp_path_factory.mktemp("build", numbered=True) + out_wasm = outdir / "out.wasm" + + cmd = [str(compiler)] + list(flags) + [ + "--target=wasm32-wasi", + "-Wl,--export=app_main", + "-Wl,--no-entry", + "-nostartfiles", + "-o", str(out_wasm), + ] + [str(apps_dir / s) for s in sources] + + p = subprocess.run(cmd, capture_output=True, text=True) + if p.returncode != 0: + raise AssertionError( + f"build failed:\n cmd: {' '.join(cmd)}\n" + f" stdout: {p.stdout}\n stderr: {p.stderr}" + ) + cache[key] = out_wasm + return out_wasm + + return _build + + +@pytest.fixture +def wasm_opt_pass(binaryen, tmp_path): + """Run wasm-opt on a wasm file and return the new path.""" + def _pass(input_wasm, args): + out = tmp_path / (input_wasm.stem + ".opt.wasm") + cmd = [str(binaryen / "bin" / "wasm-opt")] + list(args) + [ + "-o", str(out), str(input_wasm), + ] + p = subprocess.run(cmd, capture_output=True, text=True) + if p.returncode != 0: + raise AssertionError( + f"wasm-opt failed:\n cmd: {' '.join(cmd)}\n" + f" stdout: {p.stdout}\n stderr: {p.stderr}" + ) + return out + return _pass + + +@pytest.fixture +def run_addr2line(addr2line_script, wabt, tmp_path, wasi_sdk, request): + """Invoke addr2line.py and capture stdout/stderr/exitcode. + + `call_stack` may be either a list of strings (one per frame line) or + a Path to an existing call-stack file. `extra_args` is appended to + the command line (e.g. ['--mode', 'aot']). + + Under `pytest -v`/`-vv` (verbosity >= 1) the invocation and resolved + output are printed; combine with `-s` to see them live, otherwise + pytest captures them and only surfaces them on failure. + """ + verbose = request.config.getoption("verbose") >= 1 + + def _run(wasm_file, call_stack, sdk_override=None, extra_args=()): + sdk = sdk_override if sdk_override is not None else wasi_sdk + if isinstance(call_stack, (list, tuple)): + cs_file = tmp_path / "callstack.txt" + cs_file.write_text("\n".join(call_stack) + ("\n" if call_stack else "")) + else: + cs_file = Path(call_stack) + + cmd = [ + sys.executable, str(addr2line_script), + "--wasi-sdk", str(sdk), + "--wabt", str(wabt), + "--wasm-file", str(wasm_file), + *list(extra_args), + str(cs_file), + ] + p = subprocess.run(cmd, capture_output=True, text=True) + + if verbose: + test_id = request.node.name + print(f"\n--- [{test_id}] addr2line input ---") + if isinstance(call_stack, (list, tuple)): + for line in call_stack: + print(f" {line}") + else: + print(f" (from {cs_file})") + extra = " ".join(extra_args) if extra_args else "(none)" + print(f"--- [{test_id}] extra args: {extra} ---") + print(f"--- [{test_id}] addr2line stdout (rc={p.returncode}) ---") + print(p.stdout.rstrip()) + if p.stderr.strip(): + print(f"--- [{test_id}] addr2line stderr ---") + print(p.stderr.rstrip()) + print(f"--- [{test_id}] end ---") + + return p.stdout, p.stderr, p.returncode + + return _run + + +# --- Helpers ------------------------------------------------------------- + +def find_dwarf_low_pc(wasi_sdk: Path, wasm: Path, func_name: str) -> int | None: + """Find the DW_AT_low_pc of the named DW_TAG_subprogram in `wasm`. + Returns int or None if not found.""" + p = subprocess.run( + [str(wasi_sdk / "bin" / "llvm-dwarfdump"), str(wasm)], + capture_output=True, text=True, + ) + in_subp = False + low = None + for line in p.stdout.splitlines(): + s = line.strip() + if "DW_TAG_subprogram" in s: + in_subp = True + low = None + continue + if in_subp: + import re + m = re.match(r'DW_AT_low_pc\s+\((0x[0-9a-fA-F]+)\)', s) + if m: + low = int(m.group(1), 16) + continue + m = re.match(r'DW_AT_name\s+\("' + re.escape(func_name) + r'"\)', s) + if m and low is not None: + return low + if s.startswith("DW_TAG_") or s == "": + in_subp = False + low = None + return None + + +def code_section_start(wabt: Path, wasm: Path) -> int: + """Read the Code section start offset from wasm-objdump -h.""" + p = subprocess.run( + [str(wabt / "bin" / "wasm-objdump"), "-h", str(wasm)], + capture_output=True, text=True, check=True, + ) + import re + for line in p.stdout.splitlines(): + s = line.strip() + if "Code" in s and "start=" in s: + m = re.search(r"start=(0x[0-9a-fA-F]+)", s) + if m: + return int(m.group(1), 16) + raise AssertionError("Code section not found in " + str(wasm)) diff --git a/test-tools/addr2line/tests/fixtures/aot_stack.txt b/test-tools/addr2line/tests/fixtures/aot_stack.txt new file mode 100644 index 0000000000..0d3426f793 --- /dev/null +++ b/test-tools/addr2line/tests/fixtures/aot_stack.txt @@ -0,0 +1,2 @@ +#00: 0x1e0a - $f12 +#01: 0x1dcc - app_main diff --git a/test-tools/addr2line/tests/fixtures/empty_stack.txt b/test-tools/addr2line/tests/fixtures/empty_stack.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test-tools/addr2line/tests/fixtures/fast_interp_stack.txt b/test-tools/addr2line/tests/fixtures/fast_interp_stack.txt new file mode 100644 index 0000000000..51783a5b72 --- /dev/null +++ b/test-tools/addr2line/tests/fixtures/fast_interp_stack.txt @@ -0,0 +1,2 @@ +#00: 0x0001 - $f0 +#01: 0x0001 - $f1 diff --git a/test-tools/addr2line/tests/pytest.ini b/test-tools/addr2line/tests/pytest.ini new file mode 100644 index 0000000000..cca0386e57 --- /dev/null +++ b/test-tools/addr2line/tests/pytest.ini @@ -0,0 +1,5 @@ +[pytest] +markers = + multi_sdk: parametrize over all detected wasi-sdk-*-x86_64-linux installations under /opt + slow: long-running test (typically a build + run); excluded by default if running -m "not slow" +testpaths = . diff --git a/test-tools/addr2line/tests/run_tests.sh b/test-tools/addr2line/tests/run_tests.sh new file mode 100755 index 0000000000..7143e6b2fe --- /dev/null +++ b/test-tools/addr2line/tests/run_tests.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# Copyright (C) 2019 Intel Corporation. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# Thin wrapper around pytest for the addr2line.py test suite. +# +# Usage: +# ./run_tests.sh # default: single SDK from $WASI_SDK_PATH +# ./run_tests.sh -m "not slow" # only fixture-based tests +# ./run_tests.sh --multi-sdk # parametrize over /opt/wasi-sdk-* +# +# Env vars consumed: +# WASI_SDK_PATH (default /opt/wasi-sdk) +# WABT_PATH (default /opt/wabt) +# BINARYEN_PATH (default /opt/binaryen) + +set -euo pipefail +cd "$(dirname "$0")" +exec python3 -m pytest "$@" diff --git a/test-tools/addr2line/tests/test_addr2line.py b/test-tools/addr2line/tests/test_addr2line.py new file mode 100644 index 0000000000..4e322a58ca --- /dev/null +++ b/test-tools/addr2line/tests/test_addr2line.py @@ -0,0 +1,298 @@ +# Copyright (C) 2026 Intel Corporation. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +""" +Test suite for test-tools/addr2line/addr2line.py. + +Most tests build a wasm from a single apps/*.c source, capture the trap +address from DWARF, and check that addr2line.py resolves it correctly. + +Multi-SDK behavior is opt-in via `pytest --multi-sdk` — build-based +tests re-run once per detected SDK to catch DWARF-encoding changes +across clang versions. +""" + +import re +import subprocess +from pathlib import Path + +import pytest + +from conftest import find_dwarf_low_pc, code_section_start + + +# Format a wasm file offset that points at a real instruction inside +# the function body, given its DWARF low_pc and the wasm Code section +# start. Offset is `code_section_start + low_pc + delta + 1` because +# addr2line.py applies a -1 adjustment for classic-interp mode. +def _stack_line(wabt, wasm, wasi_sdk, func_name, delta=1, frame_idx=0, + func_label=None): + cs = code_section_start(wabt, wasm) + low = find_dwarf_low_pc(wasi_sdk, wasm, func_name) + if low is None: + pytest.skip(f"DW_TAG_subprogram for {func_name} not found in {wasm}") + # +1 to compensate for addr2line.py's -1 in interp mode + file_off = cs + low + delta + 1 + label = func_label or "$f0" + return f"#{frame_idx:02d}: 0x{file_off:04x} - {label}" + + +# --- Build-based tests --------------------------------------------------- + +@pytest.mark.slow +def test_simple_resolution(build_wasm, wabt, run_addr2line, wasi_sdk): + """Single-function trap resolves to function=crash, file=simple.c.""" + wasm = build_wasm(["simple.c"], ["-O0", "-g"]) + line = _stack_line(wabt, wasm, wasi_sdk, "crash", delta=1, + func_label="$f0") + out, err, rc = run_addr2line(wasm, [line]) + assert rc == 0 + assert "crash" in out + assert "simple.c" in out + + +@pytest.mark.slow +def test_inline_chain_basic(build_wasm, wabt, run_addr2line, wasi_sdk): + """always_inline helper at trap site produces 2-frame inline chain.""" + wasm = build_wasm(["always_inline.c"], ["-O0", "-g"]) + # Trap is inside `inner` (always_inline'd into outer) + line = _stack_line(wabt, wasm, wasi_sdk, "outer", delta=2, + func_label="$f0") + out, err, rc = run_addr2line(wasm, [line]) + assert rc == 0 + assert "inner" in out + assert "outer" in out + # Inline annotation must appear + assert "(inlined into" in out + + +@pytest.mark.slow +def test_inline_chain_deep(build_wasm, wabt, run_addr2line, wasi_sdk): + """4-level inline chain produces all 4 frames in order.""" + wasm = build_wasm(["deep_inline_chain.c"], ["-O0", "-g"]) + # All 4 levels inlined into app_main; pick low_pc(app_main)+small + line = _stack_line(wabt, wasm, wasi_sdk, "app_main", delta=2, + func_label="$f0") + out, err, rc = run_addr2line(wasm, [line]) + assert rc == 0 + # All four levels should appear somewhere in the output + for fn in ("level1", "level2", "level3", "level4"): + assert fn in out, f"missing {fn} in output:\n{out}" + assert "(inlined into" in out + # The OUTERMOST frame must be app_main (not a wasi-libc symbol like + # `free` / `__multi3` that overlaps app_main's PC range — that's the + # exact regression the interval-table overlay in resolve_address + # exists to prevent). + assert re.search(r"^\s+app_main\b", out, re.MULTILINE), ( + f"outermost frame is not app_main:\n{out}" + ) + + +@pytest.mark.slow +def test_cross_tu_inline(build_wasm, wasm_opt_pass, wabt, run_addr2line, + wasi_sdk): + """Multi-file recursion under -Oz -g -flto + wasm-opt -Oz -g. + + `wasm-opt -Oz -g` reorders DWARF in ways that confuse both legacy + and modern symbolizer paths on the function-name lookup; this test + pins the more limited contract that the line table itself stays + inside our sources (or returns `??:0`), never a wasi-libc path. + Outermost-name correctness for non-mangled DWARF is covered by + test_inline_chain_deep. + """ + raw = build_wasm( + ["multi_file_recur_main.c", "multi_file_recur.c"], + ["-Oz", "-g", "-flto"], + ) + debug = wasm_opt_pass(raw, ["-Oz", "-g"]) + line = _stack_line(wabt, debug, wasi_sdk, "recur", delta=0x40, + func_label="$f0") + out, err, rc = run_addr2line(debug, [line]) + assert rc == 0 + assert ( + "multi_file_recur" in out + or "??:0" in out + ), f"line table did not resolve to multi_file_recur* source:\n{out}" + + +@pytest.mark.slow +def test_trap_mid_function(build_wasm, wabt, run_addr2line, wasi_sdk): + """Trap inside a loop body resolves to the trap line, not entry line.""" + wasm = build_wasm(["trap_in_loop.c"], ["-O0", "-g"]) + # Probe well past entry so the line table catches the trap line + line = _stack_line(wabt, wasm, wasi_sdk, "crash", delta=0x20, + func_label="$f0") + out, err, rc = run_addr2line(wasm, [line]) + assert rc == 0 + assert "crash" in out + assert "trap_in_loop.c" in out + # The trap is on its own line — output should reference a line >= 14 + # (function declaration is on line ~10, trap is below it) + m = re.search(r"trap_in_loop\.c:(\d+)", out) + assert m is not None, f"no file:line in output:\n{out}" + line_num = int(m.group(1)) + assert line_num > 10, ( + f"resolved to line {line_num}, expected > 10 (post-declaration)" + ) + + +@pytest.mark.slow +def test_multi_frame_callstack(build_wasm, wabt, run_addr2line, wasi_sdk): + """Multi-frame call stack renders all frames with distinct indices.""" + wasm = build_wasm(["multi_frame.c"], ["-O0", "-g"]) + lines = [] + for idx, fn in enumerate(["bot", "mid", "top", "app_main"]): + lines.append( + _stack_line(wabt, wasm, wasi_sdk, fn, delta=2, + frame_idx=idx, func_label="$f0") + ) + out, err, rc = run_addr2line(wasm, lines) + assert rc == 0 + for fn in ("bot", "mid", "top", "app_main"): + assert fn in out, f"missing {fn} in output:\n{out}" + + +@pytest.mark.slow +def test_cxx_demangling(build_wasm, wabt, run_addr2line, wasi_sdk): + """C++ symbols come through the toolchain in a readable form. + + The DWARF that wasi-sdk's clang emits stores the demangled name in + DW_AT_name and the mangled `_Z...` form in DW_AT_linkage_name; both + llvm-symbolizer and the wasm name section emit the demangled form, + so addr2line.py's llvm-cxxfilt pass is usually a no-op. This test + asserts the end-to-end output is human-readable: source file from + our sample, line numbers present, no mangled `_Z...` leaks. + + Symbolizer-reported function names for templated/namespaced symbols + are not asserted — both clang < 22 and clang 22+ pick the wrong + DW_TAG_subprogram for some C++ template addresses on wasm targets + (e.g. reporting `app_main` for an address that DWARF clearly puts + inside `crash_with`). The line table is correct in both cases. + """ + wasm = build_wasm(["cxx_mangled.cpp"], ["-O0", "-g"], language="cxx") + # Look up by the DWARF DW_AT_name (already demangled, with template args). + line = _stack_line(wabt, wasm, wasi_sdk, "crash_with", + delta=2, func_label="$f0") + out, err, rc = run_addr2line(wasm, [line]) + assert rc == 0 + # No mangled `_Z...` should ever leak to the user. + assert "_ZN" not in out, f"mangled prefix leaked:\n{out}" + # Source file should resolve to our sample (line table is reliable). + assert "cxx_mangled.cpp" in out, ( + f"line table did not resolve to cxx_mangled.cpp:\n{out}" + ) + + +@pytest.mark.slow +def test_aot_mode(build_wasm, wasm_opt_pass, wabt, run_addr2line, wasi_sdk): + """--mode=aot uses no -1 adjustment and resolves correctly. + + For an address that's exactly at low_pc+delta (no -1), --mode=aot must + still resolve to the right function (whereas --mode=interp would + apply -1 and possibly land outside the function range). + """ + raw = build_wasm(["simple.c"], ["-Oz", "-g"]) + debug = wasm_opt_pass(raw, ["-Oz", "-g"]) + cs = code_section_start(wabt, debug) + low = find_dwarf_low_pc(wasi_sdk, debug, "crash") + if low is None: + pytest.skip("crash() inlined away under -Oz; skipping AOT mode test") + aot_offset = cs + low + 4 # AOT path uses offset verbatim + cs_line = f"#00: 0x{aot_offset:04x} - $f0" + out, err, rc = run_addr2line(debug, [cs_line], extra_args=["--mode", "aot"]) + assert rc == 0 + assert "crash" in out + + +# --- Fixture-based tests (no build) -------------------------------------- + +def test_fast_interp_mode(addr2line_script, wabt, wasi_sdk, fixtures_dir, + run_addr2line, build_wasm): + """--mode=fast-interp falls back to function-name lookup (no addr math). + + The fixture's $f0 and $f1 must resolve to simple.c's two functions + (`crash` and `app_main`) via the wasm name section. + """ + wasm = build_wasm(["simple.c"], ["-O0", "-g"]) + out, err, rc = run_addr2line( + wasm, fixtures_dir / "fast_interp_stack.txt", + extra_args=["--mode", "fast-interp"], + ) + assert rc == 0 + assert "crash" in out, f"expected $f0 -> crash:\n{out}" + assert "app_main" in out, f"expected $f1 -> app_main:\n{out}" + + +def test_no_addr_mode(build_wasm, run_addr2line, fixtures_dir): + """--no-addr resolves by function name, not address. + + Same fixture as fast-interp: $f0 -> crash, $f1 -> app_main. Under + --no-addr addr2line should additionally surface the decl-line from + DWARF (not just the function name). + """ + wasm = build_wasm(["simple.c"], ["-O0", "-g"]) + out, err, rc = run_addr2line( + wasm, fixtures_dir / "fast_interp_stack.txt", + extra_args=["--no-addr"], + ) + assert rc == 0 + assert "crash" in out, f"expected $f0 -> crash:\n{out}" + assert "app_main" in out, f"expected $f1 -> app_main:\n{out}" + assert "simple.c" in out, f"expected DWARF source file:\n{out}" + + +def test_offset_zero_fallback(build_wasm, run_addr2line): + """Runtime offset=0 falls back gracefully (no assertion crash).""" + wasm = build_wasm(["simple.c"], ["-O0", "-g"]) + out, err, rc = run_addr2line(wasm, ["#00: 0x0000 - app_main"]) + assert rc == 0 + # Output should mention app_main even with no useful offset + assert "app_main" in out + + +def test_empty_input(build_wasm, run_addr2line, fixtures_dir): + """Empty call stack file produces clean exit, minimal output.""" + wasm = build_wasm(["simple.c"], ["-O0", "-g"]) + out, err, rc = run_addr2line(wasm, fixtures_dir / "empty_stack.txt") + assert rc == 0 + + +# --- Cross-cutting ------------------------------------------------------- + + + +@pytest.mark.slow +def test_build_subprogram_intervals_filters_invalid_dies( + build_wasm, wasm_opt_pass, wasi_sdk, addr2line_script +): + """Guard against wasm-opt DCE ghosts and DW_AT_declaration DIEs + leaking into the interval table. + + Empirically, `wasm-opt -Oz -g` leaves DIEs for dead-code-eliminated + compiler-rt / libm helpers behind with low_pc=0 and tiny/empty + high_pc. Those must NOT appear as entries in the interval table. + """ + import sys + sys.path.insert(0, str(addr2line_script.parent)) + from addr2line import build_subprogram_intervals + + # Build a wasi-libc-linked wasm and post-process with wasm-opt -Oz -g + # to trigger the DCE-ghost pattern. + raw = build_wasm(["simple.c"], ["-Oz", "-g", "-flto"]) + debug = wasm_opt_pass(raw, ["-Oz", "-g"]) + dwarf_dump = wasi_sdk / "bin" / "llvm-dwarfdump" + + intervals = build_subprogram_intervals(dwarf_dump, debug) + assert intervals, "expected at least one subprogram in the interval table" + + # No entry may have low_pc == 0 (structurally impossible for real code: + # the wasm Code section never starts at file offset 0). + zero_lowpc = [(lo, hi, nm) for (lo, hi, nm) in intervals if lo == 0] + assert not zero_lowpc, ( + f"interval table contains low_pc=0 entries (wasm-opt DCE ghosts): " + f"{zero_lowpc[:5]}" + ) + + # No entry may have high_pc <= low_pc. + empty = [(lo, hi, nm) for (lo, hi, nm) in intervals if hi <= lo] + assert not empty, f"interval table contains empty ranges: {empty[:5]}" + diff --git a/tests/regression/ba-issues/build_wamr.sh b/tests/regression/ba-issues/build_wamr.sh index 9f2b3c716f..233f7bad8f 100755 --- a/tests/regression/ba-issues/build_wamr.sh +++ b/tests/regression/ba-issues/build_wamr.sh @@ -66,4 +66,8 @@ build_iwasm "-DWAMR_BUILD_BRANCH_HINTS=1" "default-branch-hints-enabled" # build default iwasm for testing tail call with fast-interp build_iwasm "-DWAMR_BUILD_REF_TYPES=1 -DWAMR_BUILD_FAST_INTERP=1 -DWAMR_BUILD_TAIL_CALL=1 -DWAMR_BUILD_LIBC_WASI=0" "default-tail-call-wasi-disabled" +# build classic-interp iwasm with ASAN and hardware bounds checks disabled +# for poll_oneoff host memory safety regressions +build_iwasm "-DWAMR_BUILD_REF_TYPES=1 -DWAMR_BUILD_FAST_INTERP=0 -DWAMR_BUILD_AOT=0 -DWAMR_BUILD_JIT=0 -DWAMR_BUILD_FAST_JIT=0 -DWAMR_BUILD_SIMD=0 -DWAMR_DISABLE_HW_BOUND_CHECK=1" "poll-oneoff-asan" + # TODO: add more version of iwasm, for example, sgx version diff --git a/tests/regression/ba-issues/issues/issue-980004/poll_oneoff_out_of_bounds_write.wasm b/tests/regression/ba-issues/issues/issue-980004/poll_oneoff_out_of_bounds_write.wasm new file mode 100644 index 0000000000..ec56c43f3d Binary files /dev/null and b/tests/regression/ba-issues/issues/issue-980004/poll_oneoff_out_of_bounds_write.wasm differ diff --git a/tests/regression/ba-issues/issues/issue-980004/poll_oneoff_out_of_bounds_write.wat b/tests/regression/ba-issues/issues/issue-980004/poll_oneoff_out_of_bounds_write.wat new file mode 100644 index 0000000000..2121e0b64e --- /dev/null +++ b/tests/regression/ba-issues/issues/issue-980004/poll_oneoff_out_of_bounds_write.wat @@ -0,0 +1,38 @@ +(module + (import "wasi_snapshot_preview1" "poll_oneoff" + (func $poll_oneoff (param i32 i32 i32 i32) (result i32))) + (import "wasi_snapshot_preview1" "proc_exit" + (func $proc_exit (param i32))) + (memory (export "memory") 1) + (func (export "_start") + ;; Two absolute clock subscriptions ensure poll_oneoff writes two events. + i32.const 0 + i64.const 1 + i64.store + i32.const 8 + i32.const 0 + i32.store8 + i32.const 40 + i32.const 1 + i32.store16 + + i32.const 48 + i64.const 2 + i64.store + i32.const 56 + i32.const 0 + i32.store8 + i32.const 88 + i32.const 1 + i32.store16 + + ;; out points to exactly one wasi_event_t at the end of memory. + i32.const 0 + i32.const 65504 + i32.const 2 + i32.const 65500 + call $poll_oneoff + drop + + i32.const 0 + call $proc_exit)) diff --git a/tests/regression/ba-issues/issues/issue-980005/poll_oneoff_out_of_bounds_read.wasm b/tests/regression/ba-issues/issues/issue-980005/poll_oneoff_out_of_bounds_read.wasm new file mode 100644 index 0000000000..30ebce2925 Binary files /dev/null and b/tests/regression/ba-issues/issues/issue-980005/poll_oneoff_out_of_bounds_read.wasm differ diff --git a/tests/regression/ba-issues/issues/issue-980005/poll_oneoff_out_of_bounds_read.wat b/tests/regression/ba-issues/issues/issue-980005/poll_oneoff_out_of_bounds_read.wat new file mode 100644 index 0000000000..cce8e8b299 --- /dev/null +++ b/tests/regression/ba-issues/issues/issue-980005/poll_oneoff_out_of_bounds_read.wat @@ -0,0 +1,28 @@ +(module + (import "wasi_snapshot_preview1" "poll_oneoff" + (func $poll_oneoff (param i32 i32 i32 i32) (result i32))) + (import "wasi_snapshot_preview1" "proc_exit" + (func $proc_exit (param i32))) + (memory (export "memory") 1) + (func (export "_start") + ;; Place exactly one subscription at the end of memory. + i32.const 65488 + i64.const 1 + i64.store + i32.const 65496 + i32.const 0 + i32.store8 + i32.const 65528 + i32.const 1 + i32.store16 + + ;; nsubscriptions=2 forces validation of the full subscription array. + i32.const 65488 + i32.const 64 + i32.const 2 + i32.const 60 + call $poll_oneoff + drop + + i32.const 0 + call $proc_exit)) diff --git a/tests/regression/ba-issues/running_config.json b/tests/regression/ba-issues/running_config.json index 9083af9014..ebde50babd 100644 --- a/tests/regression/ba-issues/running_config.json +++ b/tests/regression/ba-issues/running_config.json @@ -1853,6 +1853,38 @@ } }, + { + "deprecated": false, + "ids": [ + 980004 + ], + "runtime": "iwasm-poll-oneoff-asan", + "file": "poll_oneoff_out_of_bounds_write.wasm", + "mode": "classic-interp", + "options": "-f _start", + "argument": "", + "expected return": { + "ret code": 1, + "stdout content": "Exception: out of bounds memory access", + "description": "poll_oneoff rejects an undersized event buffer for multi-subscription WASI calls" + } + }, + { + "deprecated": false, + "ids": [ + 980005 + ], + "runtime": "iwasm-poll-oneoff-asan", + "file": "poll_oneoff_out_of_bounds_read.wasm", + "mode": "classic-interp", + "options": "-f _start", + "argument": "", + "expected return": { + "ret code": 1, + "stdout content": "Exception: out of bounds memory access", + "description": "poll_oneoff rejects an undersized subscription array for multi-subscription WASI calls" + } + }, { "deprecated": false, "ids": [ diff --git a/tests/wamr-test-suites/test_wamr.sh b/tests/wamr-test-suites/test_wamr.sh index 97dc84d548..c4b6efa903 100755 --- a/tests/wamr-test-suites/test_wamr.sh +++ b/tests/wamr-test-suites/test_wamr.sh @@ -919,8 +919,9 @@ function do_execute_in_running_mode() # keep alpha order if [[ ${ENABLE_EH} -eq 1 ]]; then - if [[ "${RUNNING_MODE}" != "classic-interp" ]]; then - echo "support exception handling in classic-interp" + if [[ "${RUNNING_MODE}" != "classic-interp" \ + && "${RUNNING_MODE}" != "fast-interp" ]]; then + echo "support exception handling in classic-interp and fast-interp" return 0; fi fi