fix(build): support pre-5.6 UAPI headers via compat.h shim #41
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
| # SPDX-License-Identifier: LGPL-2.1-or-later OR MIT | |
| name: build | |
| on: | |
| push: | |
| branches: [main] | |
| tags: ['v*'] | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| jobs: | |
| # GNU toolchain + glibc-static. Compile-check across architectures using | |
| # the same toolchain most distros ship. Artifacts are large (~800 KB) but | |
| # confirm the source builds cleanly under GCC for each target. | |
| build-glibc: | |
| name: glibc / ${{ matrix.target.arch }} | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| target: | |
| - { arch: x86_64, prefix: '', apt: '' } | |
| - { arch: aarch64, prefix: aarch64-linux-gnu, apt: 'gcc-aarch64-linux-gnu linux-libc-dev-arm64-cross libc6-dev-arm64-cross' } | |
| - { arch: armv7, prefix: arm-linux-gnueabihf, apt: 'gcc-arm-linux-gnueabihf linux-libc-dev-armhf-cross libc6-dev-armhf-cross' } | |
| - { arch: riscv64, prefix: riscv64-linux-gnu, apt: 'gcc-riscv64-linux-gnu linux-libc-dev-riscv64-cross libc6-dev-riscv64-cross' } | |
| - { arch: ppc64le, prefix: powerpc64le-linux-gnu, apt: 'gcc-powerpc64le-linux-gnu linux-libc-dev-ppc64el-cross libc6-dev-ppc64el-cross' } | |
| - { arch: s390x, prefix: s390x-linux-gnu, apt: 'gcc-s390x-linux-gnu linux-libc-dev-s390x-cross libc6-dev-s390x-cross' } | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - name: install cross-toolchain | |
| if: matrix.target.apt != '' | |
| run: | | |
| sudo apt-get update -qq | |
| sudo apt-get install -y --no-install-recommends ${{ matrix.target.apt }} | |
| - name: build | |
| run: | | |
| if [ -z "${{ matrix.target.prefix }}" ]; then | |
| make | |
| else | |
| make CC=${{ matrix.target.prefix }}-gcc LD=${{ matrix.target.prefix }}-ld | |
| fi | |
| - name: verify | |
| run: | | |
| file payload exploit exploit-passwd vulnerable | |
| echo "payload: $(stat -c%s payload) bytes" | |
| echo "exploit: $(stat -c%s exploit) bytes" | |
| echo "exploit-passwd: $(stat -c%s exploit-passwd) bytes" | |
| echo "vulnerable: $(stat -c%s vulnerable) bytes" | |
| - name: upload binaries | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: copy-fail-c-${{ matrix.target.arch }}-glibc | |
| path: | | |
| exploit | |
| exploit-passwd | |
| vulnerable | |
| retention-days: 90 | |
| # zig cc + musl-static. Produces deploy-anywhere binaries with a fully | |
| # static link and no runtime libc version dependency. zig ships musl libc | |
| # for every target; we still need GNU binutils-<arch>-cross for the | |
| # `ld -r -b binary` step that wraps the payload as a relocatable .o, | |
| # plus kernel UAPI headers for nolibc and linux/if_alg.h. | |
| build-musl: | |
| name: musl / ${{ matrix.target.arch }} | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| target: | |
| - { arch: x86_64, zig_target: x86_64-linux-musl, apt: '', ld_prefix: '', uapi_base: /usr/include, uapi_asm: /usr/include/x86_64-linux-gnu/asm } | |
| - { arch: aarch64, zig_target: aarch64-linux-musl, apt: 'binutils-aarch64-linux-gnu linux-libc-dev-arm64-cross', ld_prefix: aarch64-linux-gnu, uapi_base: /usr/aarch64-linux-gnu/include, uapi_asm: /usr/aarch64-linux-gnu/include/asm } | |
| - { arch: armv7, zig_target: arm-linux-musleabihf, apt: 'binutils-arm-linux-gnueabihf linux-libc-dev-armhf-cross', ld_prefix: arm-linux-gnueabihf, uapi_base: /usr/arm-linux-gnueabihf/include, uapi_asm: /usr/arm-linux-gnueabihf/include/asm } | |
| - { arch: riscv64, zig_target: riscv64-linux-musl, apt: 'binutils-riscv64-linux-gnu linux-libc-dev-riscv64-cross', ld_prefix: riscv64-linux-gnu, uapi_base: /usr/riscv64-linux-gnu/include, uapi_asm: /usr/riscv64-linux-gnu/include/asm } | |
| - { arch: ppc64le, zig_target: powerpc64le-linux-musl, apt: 'binutils-powerpc64le-linux-gnu linux-libc-dev-ppc64el-cross', ld_prefix: powerpc64le-linux-gnu, uapi_base: /usr/powerpc64le-linux-gnu/include, uapi_asm: /usr/powerpc64le-linux-gnu/include/asm } | |
| - { arch: s390x, zig_target: s390x-linux-musl, apt: 'binutils-s390x-linux-gnu linux-libc-dev-s390x-cross', ld_prefix: s390x-linux-gnu, uapi_base: /usr/s390x-linux-gnu/include, uapi_asm: /usr/s390x-linux-gnu/include/asm } | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2.2.1 | |
| with: | |
| version: 0.14.0 | |
| use-cache: false | |
| - name: install cross-binutils and UAPI headers | |
| if: matrix.target.apt != '' | |
| run: | | |
| sudo apt-get update -qq | |
| sudo apt-get install -y --no-install-recommends ${{ matrix.target.apt }} | |
| - name: build | |
| run: | | |
| LD_BIN=ld | |
| if [ -n "${{ matrix.target.ld_prefix }}" ]; then | |
| LD_BIN=${{ matrix.target.ld_prefix }}-ld | |
| fi | |
| make zig-musl-static \ | |
| ZIG_TARGET=${{ matrix.target.zig_target }} \ | |
| LD="$LD_BIN" \ | |
| UAPI_LINUX_DIR=${{ matrix.target.uapi_base }}/linux \ | |
| UAPI_ASM_GENERIC_DIR=${{ matrix.target.uapi_base }}/asm-generic \ | |
| UAPI_ASM_DIR=${{ matrix.target.uapi_asm }} | |
| - name: strip binaries | |
| env: | |
| LD_PREFIX: ${{ matrix.target.ld_prefix }} | |
| run: | | |
| STRIP=strip | |
| if [ -n "$LD_PREFIX" ]; then | |
| STRIP="${LD_PREFIX}-strip" | |
| fi | |
| for bin in exploit exploit-passwd vulnerable; do | |
| BEFORE=$(stat -c%s "$bin") | |
| "$STRIP" --strip-all "$bin" | |
| AFTER=$(stat -c%s "$bin") | |
| echo "$bin stripped: $BEFORE -> $AFTER bytes" | |
| done | |
| - name: verify | |
| run: | | |
| file payload exploit exploit-passwd vulnerable | |
| echo "payload: $(stat -c%s payload) bytes" | |
| echo "exploit: $(stat -c%s exploit) bytes" | |
| echo "exploit-passwd: $(stat -c%s exploit-passwd) bytes" | |
| echo "vulnerable: $(stat -c%s vulnerable) bytes" | |
| - name: upload binaries | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: copy-fail-c-${{ matrix.target.arch }}-musl | |
| path: | | |
| exploit | |
| exploit-passwd | |
| vulnerable | |
| retention-days: 90 | |
| release: | |
| if: startsWith(github.ref, 'refs/tags/v') | |
| needs: [build-glibc, build-musl] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - name: download all artifacts | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| path: artifacts | |
| - name: package release tarballs | |
| run: | | |
| mkdir -p release | |
| stagebase="$(mktemp -d)" | |
| for d in artifacts/copy-fail-c-*; do | |
| base=$(basename "$d") | |
| stage="$stagebase/$base" | |
| mkdir -p "$stage" | |
| cp "$d/exploit" "$d/exploit-passwd" "$d/vulnerable" "$stage/" | |
| cp README.md LICENSE-MIT LICENSE-LGPL "$stage/" | |
| chmod +x "$stage/exploit" "$stage/exploit-passwd" "$stage/vulnerable" | |
| tar -czf "release/${base}.tar.gz" -C "$stagebase" "$base" | |
| done | |
| ( cd release && sha256sum *.tar.gz > SHA256SUMS ) | |
| ls -la release/ | |
| - name: create release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TAG: ${{ github.ref_name }} | |
| run: | | |
| gh release create "$TAG" \ | |
| --title "$TAG" \ | |
| --notes-file - release/* <<'EOF' | |
| Cross-platform C port of CVE-2026-31431 (Copy Fail). | |
| Discovery and disclosure: Theori / Xint, https://copy.fail/ | |
| ## Downloads | |
| Each archive is named `copy-fail-c-<arch>-<libc>.tar.gz` and | |
| contains three statically-linked binaries plus README and LICENSE: | |
| * `exploit`: binary-mutation variant. Mutates a setuid binary's | |
| page cache, then execs it. | |
| * `exploit-passwd`: /etc/passwd UID-flip variant. Mutates four | |
| bytes of /etc/passwd's page cache, then execs `su`. Works where | |
| the binary-mutation route is blocked but has a narrower cashout | |
| surface; see README for details. | |
| * `vulnerable`: non-destructive vulnerability checker. Creates a | |
| local testfile and runs the AF_ALG/splice primitive against | |
| its own page cache to detect kernel susceptibility, without | |
| touching any system file. Runs unprivileged. Exits 100 if | |
| vulnerable, 0 otherwise. | |
| ## Build modes | |
| * `-glibc`: GNU cross-toolchain, glibc-static. Larger (~800 KB) | |
| but functionally identical to source-built binaries on a | |
| glibc system. | |
| * `-musl`: zig cc + musl-static. Smaller (~30-60 KB) and | |
| forward-compatible with any glibc version on the target. | |
| Architectures: x86_64, aarch64, armv7, riscv64, ppc64le, s390x. | |
| ## Verification | |
| `SHA256SUMS` lists the SHA-256 of every tarball: | |
| sha256sum -c SHA256SUMS | |
| See the [README](https://github.com/tgies/copy-fail-c/blob/main/README.md) | |
| for the kernel-version window of applicability and a description | |
| of the exploit mechanism. | |
| EOF |