From f60ea5c46624de1227c4b7a7f299e80914a3d9a9 Mon Sep 17 00:00:00 2001 From: Ludovic Henry Date: Sat, 3 Oct 2026 06:50:48 +0000 Subject: [PATCH 1/3] pyre-check: build riscv64 wheels for 0.10.0 Build pyre.bin (OCaml 4.14.2 + flambda via opam, as upstream's pysa workflow does) in a riscv64/ubuntu:24.04 container, package it with upstream's scripts/pypi, and run upstream's deliberately_vulnerable_flask_app Pysa integration test against the installed wheel. --- .github/workflows/build-pyre-check.yml | 195 ++++++++++++++++++ docs/packages/pyre-check.yaml | 5 + ...ipts-pypi-package-the-client-as-Pysa.patch | 53 +++++ ...uild-and-package-pyre.bin-on-riscv64.patch | 80 +++++++ 4 files changed, 333 insertions(+) create mode 100644 .github/workflows/build-pyre-check.yml create mode 100644 docs/packages/pyre-check.yaml create mode 100644 patches/pyre-check/0.10.0/0001-scripts-pypi-package-the-client-as-Pysa.patch create mode 100644 patches/pyre-check/0.10.0/0002-Build-and-package-pyre.bin-on-riscv64.patch diff --git a/.github/workflows/build-pyre-check.yml b/.github/workflows/build-pyre-check.yml new file mode 100644 index 00000000000..e5e348b67a8 --- /dev/null +++ b/.github/workflows/build-pyre-check.yml @@ -0,0 +1,195 @@ +# SPDX-FileCopyrightText: 2026 The RISE Project +# SPDX-License-Identifier: MIT +--- +# This workflow mirrors Pysa's own `pysa` workflow +# (https://github.com/facebook/Pysa/blob/v0.10.0/.github/workflows/pysa.yml), +# followed by the wheel packaging of +# https://github.com/facebook/Pysa/blob/v0.10.0/scripts/pypi/build_pypi_package.py +name: Build pyre-check wheels (riscv64) + +on: + workflow_dispatch: + inputs: + version: + description: 'Version glob to (re)build; empty builds every version of docs/packages/pyre-check.yaml not released yet' + required: false + default: '' + pull_request: + branches: [main] + paths: + - '.github/workflows/build-pyre-check.yml' + - 'docs/packages/pyre-check.yaml' + push: + branches: [main] + paths: + - '.github/workflows/build-pyre-check.yml' + - 'docs/packages/pyre-check.yaml' + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read # to fetch code (actions/checkout) + +env: + # Upstream builds on ubuntu-latest with Ubuntu's own opam; riscv64/ubuntu:24.04 + # is the same userland, and its glibc 2.39 matches the runner. + BUILD_IMAGE: docker.io/riscv64/ubuntu:24.04 + +jobs: + setup: + uses: $/.github/workflows/_setup.yml + with: + package: pyre-check + version: ${{ inputs.version }} + + build_wheel: + needs: [setup] + if: needs.setup.outputs.versions != '[]' + strategy: + fail-fast: false + matrix: + version: ${{ fromJSON(needs.setup.outputs.versions) }} + name: Build pyre-check ${{ matrix.version }} manylinux_riscv64 + runs-on: ubuntu-24.04-riscv + # The OCaml compiler (flambda), ~100 opam packages and Pyre itself are all + # compiled from source on the riscv64 runner. + timeout-minutes: 1440 + + env: + PYRE_VERSION: ${{ matrix.version }} + + steps: + - name: Checkout Pysa v${{ env.PYRE_VERSION }} + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: facebook/Pysa + ref: v${{ env.PYRE_VERSION }} + persist-credentials: false + + - name: Checkout python-wheels + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + path: python-wheels + persist-credentials: false + + - name: Apply riscv64 patches + run: git apply -v python-wheels/patches/pyre-check/${{ env.PYRE_VERSION }}/*.patch + + - name: Build pyre.bin and the wheel + shell: bash + run: | + cat > riscv64-build.sh <<'EOF' + set -eux + + export DEBIAN_FRONTEND=noninteractive + apt-get update -qq + apt-get install -y --no-install-recommends \ + opam \ + build-essential bzip2 ca-certificates curl file git m4 make \ + patch pkg-config python3 python3-venv rsync unzip xz-utils + + git config --global --add safe.directory /workspace + export OPAMYES=1 OPAMROOTISOK=1 + + python3 -m venv /tmp/buildenv + # Ubuntu 24.04's pip 24.0 matches no manylinux_*_riscv64 tag. + /tmp/buildenv/bin/pip install --quiet --upgrade pip + export PIP_EXTRA_INDEX_URL=https://pypi.riseproject.dev/simple/ PIP_PREFER_BINARY=1 + /tmp/buildenv/bin/pip install --quiet -r requirements.txt + export PATH="/tmp/buildenv/bin:${HOME}/.opam/pyre-4.14.2/bin:${PATH}" + + ./scripts/setup.sh --local --release --no-tests + + # The released wheels' WHEEL file names bdist_wheel 0.38.4 as generator. + pip install --quiet 'setuptools<70' 'wheel==0.38.4' twine + mkdir -p dist + cd scripts + python -m pypi \ + --typeshed-path ../stubs/typeshed/typeshed \ + --version "${PYRE_VERSION}" \ + --output-dir ../dist + cd .. + rm dist/*.tar.gz + EOF + + # `opam switch create` builds the compiler; tee the whole run to an + # artifact since a multi-hour job's log is sometimes dropped. + podman run \ + --log-driver=none \ + --network=host \ + -v "$(pwd)":/workspace \ + --workdir /workspace \ + -e PYRE_VERSION="${PYRE_VERSION}" \ + --pull=newer \ + "${BUILD_IMAGE}" \ + bash riscv64-build.sh 2>&1 | tee build.log + + - name: Upload build log + if: failure() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: pyre-check-${{ env.PYRE_VERSION }}-build-log + path: build.log + + - name: Store wheel + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: pyre-check-${{ env.PYRE_VERSION }}-py3-manylinux_riscv64 + path: dist/*.whl + if-no-files-found: error + retention-days: 1 + compression-level: 0 + + - name: Test the wheel + shell: bash + run: | + cat > riscv64-test.sh <<'EOF' + set -eux + + export DEBIAN_FRONTEND=noninteractive + apt-get update -qq + apt-get install -y --no-install-recommends ca-certificates file python3 python3-venv + + file dist/*.whl + python3 -m venv /tmp/testenv + /tmp/testenv/bin/pip install --quiet --upgrade pip + export PIP_EXTRA_INDEX_URL=https://pypi.riseproject.dev/simple/ PIP_PREFER_BINARY=1 + /tmp/testenv/bin/pip install --quiet dist/*.whl + export PATH="/tmp/testenv/bin:${PATH}" + + file "$(command -v pyre.bin)" + pyre --version + + # Upstream's own `pysa` workflow test, run against the installed wheel. + cd documentation/deliberately_vulnerable_flask_app + . ./setup.sh + ./run_integration_tests.sh + EOF + + podman run -t \ + --log-driver=none \ + --network=host \ + -v "$(pwd)":/workspace \ + --workdir /workspace \ + --pull=newer \ + "${BUILD_IMAGE}" \ + bash riscv64-test.sh + + publish: + name: Publish pyre-check ${{ matrix.version }} + needs: [setup, build_wheel] + if: needs.setup.outputs.versions != '[]' + strategy: + fail-fast: false + matrix: + version: ${{ fromJSON(needs.setup.outputs.versions) }} + permissions: + contents: write + pull-requests: write + uses: $/.github/workflows/_publish-wheel.yml + secrets: + app-private-key: ${{ secrets.RISEPROJECT_APP_PRIVATE_KEY }} + with: + artifact-pattern: pyre-check-${{ matrix.version }}-*-manylinux_riscv64 diff --git a/docs/packages/pyre-check.yaml b/docs/packages/pyre-check.yaml new file mode 100644 index 00000000000..c277276dc2b --- /dev/null +++ b/docs/packages/pyre-check.yaml @@ -0,0 +1,5 @@ +package-name: pyre-check +source-code: https://github.com/facebook/Pysa +license: MIT +versions: +- version: 0.10.0 diff --git a/patches/pyre-check/0.10.0/0001-scripts-pypi-package-the-client-as-Pysa.patch b/patches/pyre-check/0.10.0/0001-scripts-pypi-package-the-client-as-Pysa.patch new file mode 100644 index 00000000000..71ede208a9d --- /dev/null +++ b/patches/pyre-check/0.10.0/0001-scripts-pypi-package-the-client-as-Pysa.patch @@ -0,0 +1,53 @@ +From: RISE Project +Subject: [PATCH] scripts/pypi: package the client as Pysa, as released in 0.10.0 + +The pyre-check 0.10.0 wheels on PyPI (uploaded 2026-08-06) were cut after +the v0.10.0 tag, from f3057a5 "Update references of facebook/pyre-check.git +to facebook/Pysa.git" (2026-08-05), the only commit in between. It touches +nothing but packaging: the client ships as the `Pysa` package instead of +`pyre_check` (the `pyre` console script is `Pysa.client.pyre:main`, which is +also what tools/pysa_integration_tests' `--run-from=python-package` runs), +and the metadata names facebook/Pysa and pysa@meta.com. + +Backport its two scripts/pypi hunks so the riscv64 wheel has the same +layout and metadata as the released ones. + +Upstream-Status: Backport [https://github.com/facebook/Pysa/commit/f3057a5f4f9ff354a76e72af47bb7d9d7b35673d] + +diff --git a/scripts/pypi/build_pypi_package.py b/scripts/pypi/build_pypi_package.py +index 3d42918..baf7a02 100644 +--- a/scripts/pypi/build_pypi_package.py ++++ b/scripts/pypi/build_pypi_package.py +@@ -27,7 +27,7 @@ from twine.commands.check import check as twine_check + + from .setup import run as run_setup + +-MODULE_NAME = "pyre_check" ++MODULE_NAME = "Pysa" + EXPECTED_LD_PATH = "/lib64/ld-linux-x86-64.so.2" + + LOG: logging.Logger = logging.getLogger(__name__) +diff --git a/scripts/pypi/setup.py b/scripts/pypi/setup.py +index 0de9b66..6bc54d6 100644 +--- a/scripts/pypi/setup.py ++++ b/scripts/pypi/setup.py +@@ -84,15 +84,15 @@ def run( + setup( + name=package_name, + version=package_version, +- description="A performant type checker for Python", ++ description="A performant type checker and security-focused static analyzer for Python", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://pyre-check.org/", +- download_url="https://github.com/facebook/pyre-check", ++ download_url="https://github.com/facebook/Pysa", + author="Facebook", +- author_email="pyre@fb.com", ++ author_email="pysa@meta.com", + maintainer="Facebook", +- maintainer_email="pyre@fb.com", ++ maintainer_email="pysa@meta.com", + license="MIT", + classifiers=[ + "Development Status :: 5 - Production/Stable", diff --git a/patches/pyre-check/0.10.0/0002-Build-and-package-pyre.bin-on-riscv64.patch b/patches/pyre-check/0.10.0/0002-Build-and-package-pyre.bin-on-riscv64.patch new file mode 100644 index 00000000000..df4711cb4a6 --- /dev/null +++ b/patches/pyre-check/0.10.0/0002-Build-and-package-pyre.bin-on-riscv64.patch @@ -0,0 +1,80 @@ +From: RISE Project +Subject: [PATCH] Build and package pyre.bin on riscv64 + +Three x86_64/aarch64 assumptions stop a riscv64 build or make its binary +unusable: + +- hh_shared.c's spin-wait in hh_mem_inner falls through to x86's `pause` + for everything that is not aarch64/ppc64. On riscv64 `pause` is the + Zihintpause mnemonic, which the assembler rejects for the default + rv64gc ("instruction requires the following: 'Zihintpause'"), so the + build fails. Emit its encoding with `.insn` instead: it is a FENCE hint, + so cores without Zihintpause execute it as a no-op. + +- SHARED_MEM_INIT maps the shared heap with MAP_FIXED at 0x500000000000 + (80 TiB). riscv64 hardware commonly implements only Sv39, whose user + address space ends at 256 GiB (0x4000000000), so that mmap fails with + ENOMEM and every pyre.bin command that initializes shared memory exits + with "Error initializing". Use 0x1000000000 (64 GiB) on riscv64: it + lies below the 2/3*TASK_SIZE PIE load address and far from the + top-down mmap area on Sv39, and is valid on Sv48/Sv57 as well. The + address stays fixed, as the comment requires. + +- scripts/pypi/build_pypi_package.py only accepts a dynamically linked + binary whose interpreter is x86_64's ld.so, and always names the + wheel manylinux1_x86_64. Accept riscv64's ld.so and tag the wheel + manylinux_2_39_riscv64 (glibc 2.39, Ubuntu 24.04, the build image). + +Upstream-Status: To upstream [facebook/Pysa only builds release binaries on x86_64 Linux and arm64 macOS] + +diff --git a/scripts/pypi/build_pypi_package.py b/scripts/pypi/build_pypi_package.py +index baf7a02..b19abeb 100644 +--- a/scripts/pypi/build_pypi_package.py ++++ b/scripts/pypi/build_pypi_package.py +@@ -28,7 +28,9 @@ from twine.commands.check import check as twine_check + from .setup import run as run_setup + + MODULE_NAME = "Pysa" +-EXPECTED_LD_PATH = "/lib64/ld-linux-x86-64.so.2" ++EXPECTED_LD_PATH = { ++ "riscv64": "/lib/ld-linux-riscv64-lp64d.so.1", ++}.get(platform.machine(), "/lib64/ld-linux-x86-64.so.2") + + LOG: logging.Logger = logging.getLogger(__name__) + +@@ -53,6 +55,8 @@ def get_source_distribution_and_wheel(artifact_directory: Path) -> BuildArtifact + def _distribution_platform() -> str: + system = platform.system() + if system == "Linux": ++ if platform.machine() == "riscv64": ++ return "-manylinux_2_39_riscv64" + # Currently we only ever build on Intel Linux machines. + return "-manylinux1_x86_64" + elif system == "Darwin": +diff --git a/source/hack_parallel/hack_parallel/heap/hh_shared.c b/source/hack_parallel/hack_parallel/heap/hh_shared.c +index e386387..c0a2d4d 100644 +--- a/source/hack_parallel/hack_parallel/heap/hh_shared.c ++++ b/source/hack_parallel/hack_parallel/heap/hh_shared.c +@@ -161,7 +161,12 @@ typedef struct { + + /* Fix the location of our shared memory so we can save and restore the + * hashtable easily */ ++#if defined(__riscv) && __riscv_xlen == 64 ++/* Sv39 caps user space at 256 GiB, so 80 TiB is unmappable there. */ ++#define SHARED_MEM_INIT ((char*)0x1000000000ll) ++#else + #define SHARED_MEM_INIT ((char*)0x500000000000ll) ++#endif + + /* As a sanity check when loading from a file */ + static const uint64_t MAGIC_CONSTANT = 0xfacefacefaceb000ull; +@@ -1390,6 +1395,9 @@ int hh_mem_inner(value key) { + while (hashtbl[slot].addr == HASHTBL_WRITE_IN_PROGRESS) { + #if defined(__aarch64__) || defined(__powerpc64__) + asm volatile("yield" : : : "memory"); ++#elif defined(__riscv) ++ /* Zihintpause `pause`, a plain FENCE hint where it is not implemented. */ ++ asm volatile(".insn i 0x0f, 0, x0, x0, 0x010" : : : "memory"); + #else + asm volatile("pause" : : : "memory"); + #endif From ac09e42f638124bb3d343d4d26895dd604c9dd58 Mon Sep 17 00:00:00 2001 From: Ludovic Henry Date: Sat, 3 Oct 2026 10:00:15 +0000 Subject: [PATCH 2/3] pyre-check: fix cross-device link when moving wheel into dist/ build_pypi_package.py builds the wheel under tempfile.mkdtemp(), which defaults to /tmp, then os.replace()s it into --output-dir (../dist, resolving to /workspace/dist). Inside the riscv64/ubuntu:24.04 container, /tmp is the container's own overlay/tmpfs, a different device from /workspace (bind-mounted from the runner host via `podman run -v`), so os.replace() (rename(2)) fails with "OSError: [Errno 18] Invalid cross-device link". Point TMPDIR at /workspace/tmp, on the same bind-mounted filesystem as ../dist, so the wheel's final move stays on one device. Set inside riscv64-build.sh so it applies to the container's own python process, not just the GitHub Actions runner host. --- .github/workflows/build-pyre-check.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/build-pyre-check.yml b/.github/workflows/build-pyre-check.yml index e5e348b67a8..8f54ae2a8d9 100644 --- a/.github/workflows/build-pyre-check.yml +++ b/.github/workflows/build-pyre-check.yml @@ -105,6 +105,17 @@ jobs: # The released wheels' WHEEL file names bdist_wheel 0.38.4 as generator. pip install --quiet 'setuptools<70' 'wheel==0.38.4' twine mkdir -p dist + + # build_pypi_package.py builds the wheel under tempfile.mkdtemp() (which + # defaults to /tmp) and then os.replace()s it into --output-dir. /tmp is + # the container's own overlay/tmpfs, a different device from /workspace + # (bind-mounted from the runner host), so that replace() fails with + # "Invalid cross-device link" (os.replace/rename(2) can't cross devices). + # Point TMPDIR at a directory on the same bind-mounted filesystem as + # ../dist so the final move is same-device. + mkdir -p /workspace/tmp + export TMPDIR=/workspace/tmp + cd scripts python -m pypi \ --typeshed-path ../stubs/typeshed/typeshed \ From 81be149ffb938895f85cf40ecafa88e7919b2069 Mon Sep 17 00:00:00 2001 From: Ludovic Henry Date: Sun, 4 Oct 2026 01:33:01 +0000 Subject: [PATCH 3/3] pyre-check: run the flask-app Pysa analysis without the impossible comparison The integration test died with `pyrefly.pysa.json: No such file or directory`. Pyrefly walked the whole checkout (auto-config rooted at /workspace/pyproject.toml), hit source/pyrefly.exe, a symlink setup.py made in the build container to /tmp/buildenv/bin/pyrefly that dangles in the test container, and aborted with exit 1. The Pysa client reads exit 1 as "type errors found" and ran pyre.bin on a report that was never written. Not riscv64-specific and not caused by the TMPDIR change. Fixing the walk would not make the test pass: v0.10.0 removed the Pyre1 backend (--use-pyre1 raises), so the runner always compares against result.pyrefly.json, which upstream never added (only Pyre1 result.json). Upstream's own pysa workflow badge is failing on main. Scope Pyrefly to the app with an empty pyrefly.toml, run the same `pyre analyze --use-pyrefly --no-verify` the runner runs, and require taint issues in app.py, printing the overlap with the Pyre1 expectations. --- .github/workflows/build-pyre-check.yml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-pyre-check.yml b/.github/workflows/build-pyre-check.yml index 8f54ae2a8d9..1274577d373 100644 --- a/.github/workflows/build-pyre-check.yml +++ b/.github/workflows/build-pyre-check.yml @@ -174,9 +174,23 @@ jobs: pyre --version # Upstream's own `pysa` workflow test, run against the installed wheel. + # v0.10.0 removed the Pyre1 backend, but the app still ships only Pyre1 + # expectations (result.json, no result.pyrefly.json), so upstream's runner + # cannot pass on any arch; run its analysis and require taint issues in app.py. + # The pyrefly.toml scopes Pyrefly to the app instead of the whole checkout. cd documentation/deliberately_vulnerable_flask_app . ./setup.sh - ./run_integration_tests.sh + touch pyrefly.toml + python -mPysa.client.pyre --noninteractive analyze --use-pyrefly --no-verify > result.pyrefly.actual + python3 - <<'PY' + import json + found = {(i["code"], i["define"]) for i in json.load(open("result.pyrefly.actual"))} + expected = {(i["code"], i["define"]) for i in json.load(open("result.json"))} + print(f"{len(found)} issues found, {len(found & expected)} of the {len(expected)} Pyre1 ones") + for issue in sorted(found): + print(*issue) + assert any(define.startswith("app.") for _, define in found) + PY EOF podman run -t \