fix(suidhelper): close_range via raw syscall for aarch64-musl portabi… #333
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| # Cancel a previous in-progress run for the same ref when a new one starts. | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| jobs: | |
| elixir: | |
| name: Elixir (mix check) | |
| runs-on: ubuntu-latest | |
| env: | |
| # setup-beam caches hex/rebar; MIX_ENV is overridden per-step where :test | |
| # is needed (ecto + test). compile/format/credo/dialyzer run in :dev so the | |
| # dev-only deps (dialyxir, credo) are available. | |
| MIX_ENV: dev | |
| services: | |
| postgres: | |
| image: postgres:17-alpine | |
| env: | |
| POSTGRES_USER: postgres | |
| POSTGRES_PASSWORD: postgres | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd "pg_isready -U postgres" | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: erlef/setup-beam@v1 | |
| id: beam | |
| with: | |
| elixir-version: "1.20" | |
| otp-version: "28" | |
| - name: Cache deps and _build | |
| uses: actions/cache@v6 | |
| with: | |
| path: | | |
| deps | |
| _build | |
| key: ${{ runner.os }}-mix-${{ steps.beam.outputs.otp-version }}-${{ steps.beam.outputs.elixir-version }}-${{ hashFiles('mix.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-mix-${{ steps.beam.outputs.otp-version }}-${{ steps.beam.outputs.elixir-version }}- | |
| - name: Cache dialyzer PLTs | |
| uses: actions/cache@v6 | |
| with: | |
| path: priv/plts | |
| key: ${{ runner.os }}-plt-${{ steps.beam.outputs.otp-version }}-${{ steps.beam.outputs.elixir-version }}-${{ hashFiles('mix.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-plt-${{ steps.beam.outputs.otp-version }}-${{ steps.beam.outputs.elixir-version }}- | |
| - name: Install dependencies | |
| run: mix deps.get | |
| # protoc + the protoc-gen-elixir escript drive the `:grpc_gen` Mix compiler, | |
| # which generates the (gitignored) gRPC bindings before the Elixir compiler. | |
| - name: Install protoc | |
| run: sudo apt-get update && sudo apt-get install -y protobuf-compiler | |
| - name: Install protoc-gen-elixir | |
| run: mix escript.install hex protobuf 0.17.0 --force | |
| # The `:guest_agent_build` Mix compiler builds the static musl guest agent | |
| # (native/guest-agent) into priv/ on every `mix compile`; the host-arch | |
| # build is a hard requirement, so the runner's default toolchain needs the | |
| # musl std. Cross arches are best-effort and stay skipped here. | |
| - name: Install musl target for the guest-agent build | |
| run: rustup target add x86_64-unknown-linux-musl | |
| - name: Cache guest-agent cargo build | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| workspaces: native/guest-agent | |
| - name: Compile (warnings as errors) | |
| # --force matches the `mix check` alias: re-surfaces warnings even on a | |
| # cached _build, where an unchanged module would otherwise be skipped. | |
| run: mix compile --warnings-as-errors --force | |
| - name: Check formatting | |
| run: mix format --check-formatted | |
| - name: Credo | |
| run: mix credo --strict | |
| - name: Create and migrate test DB | |
| env: | |
| MIX_ENV: test | |
| # -r names the repo explicitly: ecto_repos lives in mix.exs application | |
| # env (so it works when hyper is a dependency), which the ecto.* mix | |
| # tasks don't discover here -- without -r they no-op and tests then hit | |
| # a missing database. | |
| run: | | |
| mix ecto.create -r Hyper.Img.Db.Repo | |
| mix ecto.migrate -r Hyper.Img.Db.Repo | |
| - name: Test + coverage (warnings as errors) | |
| env: | |
| MIX_ENV: test | |
| # coveralls.json wraps `mix test` (so --no-start / --warnings-as-errors | |
| # still apply) and writes cover/excoveralls.json for the Codecov upload. | |
| # --no-start: the supervision tree provisions a real Firecracker host | |
| # under /srv/hyper, unavailable on a CI runner. | |
| run: mix coveralls.json --no-start --warnings-as-errors | |
| # !cancelled() (not always()) uploads results on test PASS or FAIL -- the | |
| # point of Test Analytics is failure/flake history -- while still skipping | |
| # if the workflow was cancelled. junit_formatter writes the XML at end of | |
| # run regardless of pass/fail. report_type: test_results sends JUnit to | |
| # Test Analytics via codecov-action (the standalone test-results-action is | |
| # deprecated and pinned to Node 20). | |
| - name: Upload test results to Codecov | |
| if: ${{ !cancelled() }} | |
| uses: codecov/codecov-action@v7 | |
| with: | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| files: _build/test/junit.xml | |
| flags: elixir | |
| report_type: test_results | |
| # The artifact feeds the workflow_run publisher (test-results.yml), which | |
| # runs EnricoMi with a write token -- so the GitHub Check works even on | |
| # fork PRs, where this job's token is read-only. | |
| - name: Upload Elixir test results artifact | |
| if: ${{ !cancelled() }} | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: elixir-test-results | |
| path: _build/test/junit.xml | |
| - name: Upload coverage to Codecov | |
| uses: codecov/codecov-action@v7 | |
| with: | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| files: cover/excoveralls.json | |
| flags: elixir | |
| fail_ci_if_error: true | |
| - name: Dialyzer | |
| run: mix dialyzer | |
| shellcheck: | |
| name: ShellCheck | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| # shellcheck ships preinstalled on GitHub-hosted ubuntu runners. Lint | |
| # every tracked shell script; `git ls-files` keeps new scripts covered | |
| # without a hand-maintained list and skips untracked _build/deps. | |
| - name: ShellCheck | |
| run: git ls-files -z '*.sh' | xargs -0 -r shellcheck | |
| rust: | |
| name: Rust (suidhelper) | |
| runs-on: ubuntu-latest | |
| defaults: | |
| run: | |
| working-directory: native/suidhelper | |
| steps: | |
| - uses: actions/checkout@v7 | |
| # rustup reads native/suidhelper/rust-toolchain.toml on the first cargo | |
| # call and installs the pinned nightly toolchain, components, and targets. | |
| - name: Show toolchain | |
| run: rustup show | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| workspaces: native/suidhelper | |
| - name: rustfmt | |
| run: cargo fmt --check | |
| - name: clippy | |
| run: cargo clippy --all-targets --all-features -- -D warnings | |
| - name: Install cargo-llvm-cov and nextest | |
| uses: taiki-e/install-action@v2 | |
| with: | |
| tool: cargo-llvm-cov,nextest | |
| - name: test + coverage | |
| # `nextest` runs the suite while llvm-cov collects coverage AND nextest | |
| # writes target/nextest/ci/junit.xml (see .config/nextest.toml). One run, | |
| # two artifacts: lcov.info for coverage, junit.xml for Test Analytics. | |
| run: cargo llvm-cov nextest --profile ci --all-features --lcov --output-path lcov.info | |
| # !cancelled() uploads on pass OR fail (see elixir job). The action runs at | |
| # repo root, so the path is fully qualified -- working-directory only | |
| # affects `run:` steps, not `uses:` actions. report_type: test_results | |
| # sends JUnit via codecov-action (test-results-action is deprecated/Node 20). | |
| - name: Upload test results to Codecov | |
| if: ${{ !cancelled() }} | |
| uses: codecov/codecov-action@v7 | |
| with: | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| files: native/suidhelper/target/nextest/ci/junit.xml | |
| flags: rust | |
| report_type: test_results | |
| # Repo-relative path: upload-artifact is an action, so the rust job's | |
| # `working-directory: native/suidhelper` default does NOT apply. | |
| - name: Upload Rust test results artifact | |
| if: ${{ !cancelled() }} | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: rust-test-results | |
| path: native/suidhelper/target/nextest/ci/junit.xml | |
| - name: Upload coverage to Codecov | |
| uses: codecov/codecov-action@v7 | |
| with: | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| # working-directory only applies to `run:` steps, not actions. | |
| files: native/suidhelper/lcov.info | |
| flags: rust | |
| fail_ci_if_error: true | |
| suidhelper-privileged: | |
| name: Rust suidhelper (privileged E2E) | |
| runs-on: ubuntu-latest | |
| defaults: | |
| run: | |
| working-directory: native/suidhelper | |
| steps: | |
| - uses: actions/checkout@v7 | |
| # rustup reads native/suidhelper/rust-toolchain.toml and installs the pinned | |
| # nightly on the first cargo call. | |
| - name: Show toolchain | |
| run: rustup show | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| workspaces: native/suidhelper | |
| - name: Install cargo-llvm-cov and nextest | |
| uses: taiki-e/install-action@v2 | |
| with: | |
| tool: cargo-llvm-cov,nextest | |
| # Root-gated cases are tagged by name: every test that needs root ends in | |
| # `_as_root` and self-skips when run unprivileged. We select them by that | |
| # suffix (via sudo -E so the runner's cargo/rustup home and PATH are kept) | |
| # so the root-only paths -- sys-test ok, fake-bin argv capture, mknod/chown | |
| # jail build -- actually run. A new root test is picked up automatically by | |
| # following the naming convention; no test-binary enumeration to maintain. | |
| # The rest of the suite (owner-axis tests assume a non-root uid) stays in | |
| # the non-root `rust` job above. | |
| # Coverage here is the only place the root-gated paths (tool dispatch, | |
| # sys-test, jail build) execute -- the non-root rust job's e2e tests | |
| # self-skip them. Task 1's LLVM_PROFILE_FILE passthrough makes the | |
| # spawned helper's profile reach the collector. | |
| - name: Gated tests as root + coverage | |
| run: | | |
| sudo -E env "PATH=$PATH" \ | |
| cargo llvm-cov nextest --features insecure_test_seams \ | |
| -E 'test(/_as_root$/)' --lcov --output-path lcov-privileged.info | |
| - name: Upload coverage to Codecov | |
| uses: codecov/codecov-action@v7 | |
| with: | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| # working-directory only applies to `run:` steps, not actions. | |
| files: native/suidhelper/lcov-privileged.info | |
| flags: rust | |
| fail_ci_if_error: true | |
| integration: | |
| name: Integration (KVM E2E) | |
| # ubuntu-latest x64 exposes /dev/kvm (all Linux runners with 2+ vCPUs, | |
| # per GitHub's Apr 2024 changelog); arm64 runners do NOT -- keep this x64. | |
| runs-on: ubuntu-latest | |
| # Worst case: three 10-minute tests (vm_lifecycle + crash_recovery + fork), | |
| # plus the gRPC contract wrapper's own 25-minute cap (20-min lifecycle | |
| # vitest test + errors suite + npm ci/gen), plus ~5 minutes of | |
| # provisioning/compile/npm setup. Budget with headroom above that sum: a | |
| # tight budget could cancel the job right as it finishes, and | |
| # `!cancelled()` steps then skip result uploads -- exactly when flake | |
| # history matters most. | |
| timeout-minutes: 75 | |
| env: | |
| MIX_ENV: test | |
| # Task 6's xtask seam: instrument ONLY the suidhelper build so the live | |
| # E2E's helper executions produce coverage profiles. Job-level so the | |
| # stamp compiler (mix compile) and suidhelper.install build identically. | |
| HYPER_SUIDHELPER_INSTRUMENT_COVERAGE: "1" | |
| services: | |
| postgres: | |
| image: postgres:17-alpine | |
| env: | |
| POSTGRES_USER: postgres | |
| POSTGRES_PASSWORD: postgres | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd "pg_isready -U postgres" | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| # Fail fast and loudly if GitHub ever withdraws nested virt, rather | |
| # than timing out inside a VM boot. | |
| - name: Assert KVM is available | |
| run: '[ -e /dev/kvm ]' | |
| # Node for the TypeScript gRPC contract suite (test/grpc), which the | |
| # :integration wrapper test shells out to. | |
| - uses: actions/setup-node@v5 | |
| with: | |
| node-version: "22" | |
| cache: npm | |
| cache-dependency-path: test/grpc/package-lock.json | |
| - name: Install gRPC contract suite deps | |
| run: npm ci | |
| working-directory: test/grpc | |
| # tsc catches proto/type drift in seconds, before ~10 minutes of KVM | |
| # provisioning; vitest alone strips types without checking them. | |
| - name: Typecheck gRPC contract suite | |
| run: npm run typecheck | |
| working-directory: test/grpc | |
| - uses: erlef/setup-beam@v1 | |
| id: beam | |
| with: | |
| elixir-version: "1.20" | |
| otp-version: "28" | |
| # Separate cache key from the elixir job: this _build is test-env only | |
| # and the two jobs would otherwise race the same cache entry. | |
| - name: Cache deps and _build | |
| uses: actions/cache@v6 | |
| with: | |
| path: | | |
| deps | |
| _build | |
| key: ${{ runner.os }}-mix-integration-${{ steps.beam.outputs.otp-version }}-${{ steps.beam.outputs.elixir-version }}-${{ hashFiles('mix.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-mix-integration-${{ steps.beam.outputs.otp-version }}-${{ steps.beam.outputs.elixir-version }}- | |
| - name: Install dependencies | |
| run: mix deps.get | |
| # protoc + protoc-gen-elixir drive the :grpc_gen Mix compiler (same as | |
| # the elixir job). | |
| - name: Install protoc | |
| run: sudo apt-get update && sudo apt-get install -y protobuf-compiler | |
| - name: Install protoc-gen-elixir | |
| run: mix escript.install hex protobuf 0.17.0 --force | |
| - name: Install musl target for the guest-agent build | |
| run: rustup target add x86_64-unknown-linux-musl | |
| - name: Cache rust builds | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| workspaces: | | |
| native/guest-agent | |
| native/suidhelper | |
| - name: Compile (warnings as errors) | |
| run: mix compile --warnings-as-errors | |
| # After compile: firecracker.install / suidhelper.install are mix tasks, | |
| # and the helper's checksum stamp must come from this build. | |
| - name: Provision Firecracker host | |
| run: .github/scripts/provision-kvm-host.sh | |
| - name: Create and migrate test DB | |
| run: | | |
| mix ecto.create -r Hyper.Img.Db.Repo | |
| mix ecto.migrate -r Hyper.Img.Db.Repo | |
| # No --no-start: the app boots its real supervision tree, which runs | |
| # Hyper.Node.test_system/0 host validation before any test executes. | |
| # Multiple --only flags OR together: this runs ONLY the gated tags. | |
| # coveralls.json wraps `mix test`, so the tag filters and | |
| # --warnings-as-errors pass through; it writes cover/excoveralls.json, | |
| # which is this job's Codecov upload -- without it the integration-only | |
| # modules (e.g. Hyper.Metering.Usage) report zero coverage. | |
| # LLVM_PROFILE_FILE reaches the helper through the BEAM's System.cmd | |
| # (inherited env). %p keeps concurrent helper invocations' profiles | |
| # distinct; %m makes the runtime merge same-binary runs safely; %c is | |
| # continuous mode (counters mmap-written as they increment), pairing | |
| # with the seam's runtime-counter-relocation so the jailer's | |
| # execve/_exit paths — which never reach the atexit dump — still count. | |
| - name: Integration + external tests | |
| env: | |
| LLVM_PROFILE_FILE: ${{ github.workspace }}/cover/suidhelper-%p-%m%c.profraw | |
| run: | | |
| mkdir -p cover | |
| mix coveralls.json --only integration --only external --warnings-as-errors | |
| - name: Upload test results to Codecov | |
| if: ${{ !cancelled() }} | |
| uses: codecov/codecov-action@v7 | |
| with: | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| files: _build/test/junit.xml | |
| flags: integration | |
| report_type: test_results | |
| # test-results.yml globs artifacts/**/*.xml, so this artifact joins the | |
| # unified Test Results check automatically. | |
| - name: Upload integration test results artifact | |
| if: ${{ !cancelled() }} | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: integration-test-results | |
| path: _build/test/junit.xml | |
| - name: Upload TS contract test results to Codecov | |
| if: ${{ !cancelled() }} | |
| uses: codecov/codecov-action@v7 | |
| with: | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| files: test/grpc/junit.xml | |
| flags: grpc-ts | |
| report_type: test_results | |
| # test-results.yml globs artifacts/**/*.xml, so the vitest report joins | |
| # the unified Test Results check automatically. | |
| - name: Upload TS contract test results artifact | |
| if: ${{ !cancelled() }} | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: grpc-ts-test-results | |
| path: test/grpc/junit.xml | |
| - name: Upload coverage to Codecov | |
| uses: codecov/codecov-action@v7 | |
| with: | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| files: cover/excoveralls.json | |
| flags: integration | |
| fail_ci_if_error: true | |
| # Merge the instrumented helper's profiles and export lcov, so the | |
| # live-VM paths (losetup attach, dmsetup, thin_dump, jail build) count | |
| # toward Rust coverage. llvm-tools must come from the same pinned | |
| # nightly that built the helper: profraw formats are version-locked. | |
| # The positional src/ filter keeps dependency crates out of the report; | |
| # the sed rewrites absolute SF: paths to repo-relative for codecov. | |
| - name: Export suidhelper E2E coverage | |
| working-directory: native/suidhelper | |
| run: | | |
| ls "$GITHUB_WORKSPACE"/cover/suidhelper-*.profraw | |
| rustup component add llvm-tools | |
| TOOLS="$(rustc --print sysroot)/lib/rustlib/x86_64-unknown-linux-gnu/bin" | |
| "$TOOLS/llvm-profdata" merge -sparse \ | |
| "$GITHUB_WORKSPACE"/cover/suidhelper-*.profraw \ | |
| -o suidhelper.profdata | |
| "$TOOLS/llvm-cov" export --format=lcov \ | |
| --instr-profile=suidhelper.profdata \ | |
| target/release/hyper-suidhelper \ | |
| "$GITHUB_WORKSPACE/native/suidhelper/src" \ | |
| > lcov-e2e.info | |
| sed -i "s|^SF:$GITHUB_WORKSPACE/|SF:|" lcov-e2e.info | |
| grep -c '^SF:' lcov-e2e.info | |
| - name: Upload suidhelper E2E coverage to Codecov | |
| uses: codecov/codecov-action@v7 | |
| with: | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| files: native/suidhelper/lcov-e2e.info | |
| flags: rust | |
| fail_ci_if_error: true | |
| # Uploads the triggering event payload so the workflow_run publisher | |
| # (test-results.yml) can map results back to the originating PR -- required | |
| # for PR comments on fork PRs. Tiny job, always runs. | |
| event_file: | |
| name: Upload event file | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Upload | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: Event File | |
| path: ${{ github.event_path }} |