diff --git a/.github/workflows/smoke-linux.yml b/.github/workflows/smoke-linux.yml new file mode 100644 index 0000000..5534210 --- /dev/null +++ b/.github/workflows/smoke-linux.yml @@ -0,0 +1,66 @@ +name: smoke-linux + +# Runtime smoke test for the SHS C seam. Unlike build-linux (compile-only), this +# actually runs a short kofta-fuzz campaign and asserts the seam fires at +# runtime: afl-fuzz launches the `kofta-shs serve` co-process, streams NDJSON, +# and gets candidates back (a "shs_cand,..." line in the KOFTA_DEBUG log). +# +# Runs in an Ubuntu 20.04 container (glibc 2.31) on the native x86_64 runner. +# The OS version is load-bearing: KOFTA's __args_leak relies on glibc <=2.33's +# startup stack layout (see the `container:` note below), so 22.04/glibc 2.35 +# breaks the forkserver. 20.04 ships clang-12, satisfying both KOFTA's legacy +# LLVM-pass requirement and the old-glibc requirement. run-smoke.sh builds in +# place via SMOKE_BUILD and asserts the SHS serve co-process fires end-to-end. + +on: + push: + paths: + - "**.c" + - "**.h" + - "llvm_mode/**" + - "Makefile" + - "docker/**" + - "kofta-shs" + - "shs/**" + - ".github/workflows/smoke-linux.yml" + pull_request: + paths: + - "**.c" + - "**.h" + - "llvm_mode/**" + - "Makefile" + - "docker/**" + - "kofta-shs" + - "shs/**" + - ".github/workflows/smoke-linux.yml" + +jobs: + smoke: + runs-on: ubuntu-latest + # KOFTA's __args_leak (llvm_mode/kofta-llvm-rt.o.c) grabs argv/argc with a + # hardcoded stack offset ("lea 0x50(%rsp)", +0xc) tuned for glibc <=2.33's + # __libc_csu_init startup. glibc 2.34 (Ubuntu 22.04) refactored startup, so + # the offset points at garbage there and the forkserver dies on the dry run. + # Run inside an Ubuntu 20.04 container (glibc 2.31) -- the toolchain the + # KOFTA authors developed on -- where the offset is valid. + container: ubuntu:20.04 + steps: + - uses: actions/checkout@v4 + + - name: Install clang/llvm 12 + tooling + env: + DEBIAN_FRONTEND: noninteractive + run: | + apt-get update + apt-get install -y --no-install-recommends \ + clang-12 llvm-12-dev make libc6-dev python3 ca-certificates + + # Build in place (SMOKE_BUILD == checkout), scratch in the runner temp. + # The script asserts a "shs_cand" line and exits non-zero if the seam + # never fired, so a green job means the serve co-process really ran. + - name: Run SHS runtime smoke test + run: | + SMOKE_REPO="$GITHUB_WORKSPACE" \ + SMOKE_BUILD="$GITHUB_WORKSPACE" \ + SMOKE_WORK="$RUNNER_TEMP/smoke-work" \ + bash docker/run-smoke.sh diff --git a/docker/Dockerfile b/docker/Dockerfile index 0d99346..5480e83 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,17 +1,22 @@ # Local runtime verification image for the SHS C path. # -# clang/llvm 12 is mandatory: KOFTA's LLVM passes register via the legacy pass -# manager (RegisterStandardPasses) and use the single-arg CreateLoad overload. -# clang 13+ defaults to the new PM and silently skips our instrumentation; -# LLVM 14 dropped the single-arg CreateLoad. Ubuntu 22.04 ships clang-12. -# KOFTA's runtime (kofta-llvm-rt.o.c) leaks argv/argc via hardcoded x86_64 -# inline asm ("lea 0x50(%rsp)"), so this image must run on an x86_64 Docker +# Ubuntu 20.04 is load-bearing (not just for clang-12). KOFTA's runtime +# (kofta-llvm-rt.o.c) leaks argv/argc via a hardcoded stack offset +# ("lea 0x50(%rsp)", +0xc) tuned for glibc <=2.33's __libc_csu_init startup. +# glibc 2.34 (Ubuntu 22.04) refactored startup, so on 22.04 the offset points +# at garbage and the forkserver dies on the dry run before any fuzzing happens. +# 20.04 ships glibc 2.31 (offset valid) AND clang-12, which is also mandatory: +# KOFTA's LLVM passes register via the legacy pass manager (RegisterStandard- +# Passes) and use the single-arg CreateLoad overload; clang 13+ defaults to the +# new PM and silently skips our instrumentation, and LLVM 14 dropped single-arg +# CreateLoad. +# The argv-leak asm is x86_64-only, so this image must run on an x86_64 Docker # host. On Apple Silicon, start Colima as an x86_64 VM (see docker/smoke.sh): # colima start --arch x86_64 # We deliberately do NOT use `FROM --platform=...` -- that needs BuildKit/buildx # for cross-builds, which the brew `docker` CLI doesn't ship. A native x86_64 VM # builds this natively with the classic builder instead. -FROM ubuntu:22.04 +FROM ubuntu:20.04 ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && apt-get install -y --no-install-recommends \ diff --git a/docker/run-smoke.sh b/docker/run-smoke.sh index 68fb532..842e68d 100755 --- a/docker/run-smoke.sh +++ b/docker/run-smoke.sh @@ -1,27 +1,42 @@ #!/usr/bin/env bash -# Runs inside the container (see docker/Dockerfile CMD). Builds KOFTA from the -# read-only /repo bind mount, compiles the tiny instrumented target, runs a -# short kofta-fuzz campaign with the SHS C seam launching one long-lived -# `kofta-shs serve` co-process (NDJSON over a pipe) against the offline --mock -# client, and asserts that the seam actually fired. +# Builds KOFTA, compiles the tiny instrumented target, runs a short kofta-fuzz +# campaign with the SHS C seam launching one long-lived `kofta-shs serve` +# co-process (NDJSON over a pipe) against the offline --mock client, and asserts +# that the seam actually fired. +# +# Two ways to run, both on a native x86_64 Linux host (KOFTA's runtime has +# x86_64-only argv-leak asm; arm64 emulation breaks the forkserver): +# * In the verification container (docker/Dockerfile CMD): /repo is a +# read-only bind mount, so it is copied into a writable $BUILD first. +# * Directly on a CI runner (ubuntu-22.04 is native x86_64 + clang-12): point +# SMOKE_BUILD at the already-writable checkout and the copy is skipped. +# +# Overridable via env (defaults match the container layout): +# SMOKE_REPO source tree (default /repo) +# SMOKE_BUILD writable build dir (default /build; set == repo to build in place) +# SMOKE_WORK scratch dir for the run (default /work) # # PASS criterion: the KOFTA_DEBUG log contains at least one "shs_cand,..." line, # proving afl-fuzz.c queried kofta-shs and got candidates back. Finding the # planted crash is a bonus (printed but not required, since fork-timing varies). set -euo pipefail -REPO=/repo -BUILD=/build -WORK=/work +REPO="${SMOKE_REPO:-/repo}" +BUILD="${SMOKE_BUILD:-/build}" +WORK="${SMOKE_WORK:-/work}" -echo "==> copying repo (read-only mount) into writable $BUILD" -rm -rf "$BUILD" -cp -r "$REPO" "$BUILD" +if [ "$BUILD" != "$REPO" ]; then + echo "==> copying repo ($REPO) into writable $BUILD" + rm -rf "$BUILD" + cp -r "$REPO" "$BUILD" +else + echo "==> building in place at $BUILD (no copy)" +fi cd "$BUILD" echo "==> building afl-fuzz (KOFTA_DEBUG=1)" -# AFL_NO_X86 skips the legacy GCC-mode x86 assembly self-test; the container is -# arm64 and we only use the arch-independent llvm_mode (afl-clang-fast) path. +# AFL_NO_X86 skips the legacy GCC-mode x86 assembly self-test; we only use the +# llvm_mode (afl-clang-fast) path, so the gcc-mode self-test is irrelevant here. make clean >/dev/null AFL_NO_X86=1 make CC=clang-12 KOFTA_DEBUG=1