From efcf30cfabb5c8624f445cbba5fd9036a57e50a4 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Thu, 7 May 2026 16:54:46 +0800 Subject: [PATCH] Add GitHub Actions CI for lint and build Parallel jobs run on ubuntu-24.04: lint, userspace build/test, and out-of-tree kernel module compile. Lint runs scripts under .ci/ : clang-format-20 conformance, trailing newline check (broadened beyond C/H to cover shell, Python, Markdown, and Makefiles), banned-function and secret scanning, and cppcheck on userspace sources only. Kernel code is excluded from cppcheck because Linux kernel macros produce extensive false positives without per-file suppressions; kernel-side correctness is enforced by the kernel build itself. The kmod build installs linux-headers-generic and selects the newest version under /usr/src rather than pinning to linux-headers of running kernel. GitHub-hosted runners often use Azure-flavored kernels whose exact headers package is not in default apt repo. This is compile validation only and never insmods, so any recent generic header set is acceptable. Integration coverage (sudo make check, requiring insmod of vpipe + vivid + dma-heap) is intentionally left out of CI. Concurrency cancels superseded PR runs but lets push runs on main complete so every commit keeps a green or red signal. All jobs declare timeout-minutes and the workflow holds least-privilege contents:read permission. Change-Id: Iacf9d00170440fa75e599d9eeb0a63f1a3a1544e --- .ci/check-cppcheck.sh | 33 ++++++++++++ .ci/check-format.sh | 28 ++++++++++ .ci/check-newline.sh | 20 +++++++ .ci/check-security.sh | 43 +++++++++++++++ .github/workflows/main.yml | 107 +++++++++++++++++++++++++++++++++++++ user/vpipe-dump-pgm.c | 6 ++- user/vpipe-unit-test.c | 16 +++--- 7 files changed, 246 insertions(+), 7 deletions(-) create mode 100755 .ci/check-cppcheck.sh create mode 100755 .ci/check-format.sh create mode 100755 .ci/check-newline.sh create mode 100755 .ci/check-security.sh create mode 100644 .github/workflows/main.yml diff --git a/.ci/check-cppcheck.sh b/.ci/check-cppcheck.sh new file mode 100755 index 0000000..cc34799 --- /dev/null +++ b/.ci/check-cppcheck.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +# Run cppcheck static analysis on vpipe userspace sources. +# +# Kernel code under kmod/ is intentionally excluded: cppcheck's analysis of +# Linux kernel macros (per_cpu, container_of, EXPORT_SYMBOL_GPL, sparse +# annotations) produces extensive false positives without per-file +# suppressions. Kernel-side correctness is enforced by the kernel build +# (-Wall -Werror via kbuild) and sparse where applicable. +# +# CI mode: --max-configs=1 + --enable=warning for speed. + +set -e -u -o pipefail + +mapfile -t SOURCES < <(git ls-files -z -- 'user/*.c' | tr '\0' '\n') + +if [ ${#SOURCES[@]} -eq 0 ]; then + echo "No tracked userspace C source files found." + exit 0 +fi + +# 120s budget is generous; expected runtime <30s with --max-configs=1. +timeout 120 cppcheck \ + -Iuser -Ikmod \ + --platform=unix64 \ + --enable=warning \ + --max-configs=1 --error-exitcode=1 --inline-suppr \ + --suppress=checkersReport --suppress=unmatchedSuppression \ + --suppress=missingIncludeSystem --suppress=noValidConfiguration \ + --suppress=normalCheckLevelMaxBranches \ + --suppress=preprocessorErrorDirective \ + -D_GNU_SOURCE -D__linux__ \ + "${SOURCES[@]}" diff --git a/.ci/check-format.sh b/.ci/check-format.sh new file mode 100755 index 0000000..2dbc08e --- /dev/null +++ b/.ci/check-format.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +# Verify clang-format-20 conformance for tracked vpipe C/H sources. +# Pinned to clang-format-20: older versions diverge on the .clang-format +# style, which would cause CI flakes when the runner image rolls forward. + +set -e -u -o pipefail + +# Default to clang-format-20; allow override for local runs (e.g. when a +# distro packages the same version under a different binary name). +CLANG_FORMAT="${CLANG_FORMAT:-clang-format-20}" + +if ! command -v "$CLANG_FORMAT" >/dev/null 2>&1; then + echo "Error: $CLANG_FORMAT not found (clang-format-20 is required; older versions differ in style)" >&2 + exit 1 +fi + +ret=0 +while IFS= read -r -d '' file; do + expected=$(mktemp) + "$CLANG_FORMAT" "$file" >"$expected" 2>/dev/null + if ! diff -u -p --label="$file" --label="expected coding style" "$file" "$expected"; then + ret=1 + fi + rm -f "$expected" +done < <(git ls-files -z -- 'kmod/*.c' 'kmod/*.h' 'user/*.c' 'user/*.h') + +exit $ret diff --git a/.ci/check-newline.sh b/.ci/check-newline.sh new file mode 100755 index 0000000..07cc48c --- /dev/null +++ b/.ci/check-newline.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +# Ensure tracked text sources end with a trailing newline. +# Covers C/H, shell, Python, Markdown, and Makefiles -- broader than kbox +# because vpipe ships scripts and docs alongside source. + +set -e -u -o pipefail + +ret=0 +while IFS= read -rd '' f; do + # Skip binaries (file --mime-encoding emits "binary" for those). + if file --mime-encoding "$f" | grep -qv binary; then + if [ -n "$(tail -c1 <"$f")" ]; then + echo "Warning: No newline at end of file $f" + ret=1 + fi + fi +done < <(git ls-files -z -- '*.c' '*.h' '*.sh' '*.py' '*.md' 'Makefile' '*/Makefile') + +exit $ret diff --git a/.ci/check-security.sh b/.ci/check-security.sh new file mode 100755 index 0000000..4b3a08d --- /dev/null +++ b/.ci/check-security.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash + +# Security checks for vpipe source files (kmod/ and user/). +# +# 1. Banned functions -- unsafe libc / kernel calls with safer alternatives +# (kernel-side: prefer strscpy/scnprintf; userspace: prefer strn* family). +# 2. Credential / secret patterns -- catch accidental key leaks. +# 3. Dangerous preprocessor -- detect disabled hardening features. + +set -u -o pipefail + +failed=0 + +banned='(^|[^[:alnum:]_])(gets|sprintf|vsprintf|strcpy|stpcpy|strcat|atoi|atol|atoll|atof|mktemp|tmpnam|tempnam)[[:space:]]*\(' +secrets='(password|secret|api_key|private_key|token)[[:space:]]*=[[:space:]]*"[^"]+' +dangerous_pp='#[[:space:]]*(undef|define)[[:space:]]+((_FORTIFY_SOURCE[[:space:]]+0)|(__SSP__))' +comment_only='^[[:space:]]*(//|/\*|\*|\*/)' + +while IFS= read -r -d '' f; do + code=$(grep -vE "$comment_only" "$f") + + if echo "$code" | grep -qE "$banned"; then + echo "Banned function in $f:" + grep -nE "$banned" "$f" | grep -vE "$comment_only" + failed=1 + fi + if echo "$code" | grep -iqE "$secrets"; then + echo "Possible hardcoded secret in $f:" + grep -inE "$secrets" "$f" | grep -vE "$comment_only" + failed=1 + fi + if echo "$code" | grep -qE "$dangerous_pp"; then + echo "Dangerous preprocessor directive in $f:" + grep -nE "$dangerous_pp" "$f" | grep -vE "$comment_only" + failed=1 + fi +done < <(git ls-files -z -- 'kmod/*.c' 'kmod/*.h' 'user/*.c' 'user/*.h') + +if [ $failed -eq 0 ]; then + echo "Security checks passed." +fi + +exit $failed diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..74c7a8a --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,107 @@ +# Build and validate vpipe. +# CI stays Linux-only and limits itself to checks that are reproducible on +# GitHub-hosted runners: style + static analysis, userspace build/tests, and +# kmod compilation. Full integration remains on the Linux guest via +# `sudo make check`. +# +# Parallelism (3 independent jobs): +# lint -- clang-format-20, newline, security, cppcheck +# user-build-and-test -- userspace build + unit tests (no V4L2 device) +# build-kmod -- out-of-tree module compile against generic headers +name: Build vpipe + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +# Cancel superseded PR runs to save runner minutes; let push runs on main +# complete so every commit on the branch keeps a green/red signal. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +defaults: + run: + shell: bash + +env: + DEBIAN_FRONTEND: noninteractive + +jobs: + lint: + name: Lint + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - name: Check out repository + uses: actions/checkout@v6 + + - name: Install tools + run: | + set -euo pipefail + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + clang-format-20 cppcheck file + + - name: Check clang-format + run: .ci/check-format.sh + + - name: Check trailing newline + run: .ci/check-newline.sh + + - name: Security checks + run: .ci/check-security.sh + + - name: Static analysis (cppcheck) + run: .ci/check-cppcheck.sh + + user-build-and-test: + name: Userspace Build And Test + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - name: Check out repository + uses: actions/checkout@v6 + + - name: Build userspace tools + run: make -C user + + - name: Run userspace unit tests + run: make -C user test + + build-kmod: + name: Kernel Module Build + runs-on: ubuntu-24.04 + timeout-minutes: 15 + steps: + - name: Check out repository + uses: actions/checkout@v6 + + # Use linux-headers-generic, not linux-headers-$(uname -r): hosted + # runners often run an Azure-flavored kernel whose exact headers + # package isn't in the default Ubuntu apt repo. This job only + # validates that the module compiles, never insmods, so any recent + # generic header set is acceptable. + - name: Install kernel headers + run: | + set -euo pipefail + sudo apt-get update + sudo apt-get install -y --no-install-recommends linux-headers-generic + + - name: Build kernel module + run: | + set -euo pipefail + KDIR=$(ls -1d /usr/src/linux-headers-*-generic 2>/dev/null \ + | sort -V | tail -n1) + if [ -z "$KDIR" ]; then + echo "no linux-headers-*-generic found under /usr/src" >&2 + exit 1 + fi + echo "building against KDIR=$KDIR" + make -C kmod KDIR="$KDIR" diff --git a/user/vpipe-dump-pgm.c b/user/vpipe-dump-pgm.c index a820355..11aaf5d 100644 --- a/user/vpipe-dump-pgm.c +++ b/user/vpipe-dump-pgm.c @@ -31,10 +31,14 @@ int main(int argc, char **argv) } data = malloc(size); - if (!data) + if (!data) { + fclose(fp); return 1; + } if (fread(data, 1, size, fp) != size) { perror("fread"); + fclose(fp); + free(data); return 1; } fclose(fp); diff --git a/user/vpipe-unit-test.c b/user/vpipe-unit-test.c index 86608a3..f534cc0 100644 --- a/user/vpipe-unit-test.c +++ b/user/vpipe-unit-test.c @@ -78,16 +78,20 @@ static void test_pgm_rejects_bad_dims(char *root) snprintf(path, sizeof(path), "%s/zero.pgm", root); fp = fopen(path, "wb"); EXPECT(fp != NULL); - fprintf(fp, "P5\n0 0\n255\n"); - fclose(fp); - EXPECT(vpipe_read_pgm(path, &img) < 0); + if (fp) { + fprintf(fp, "P5\n0 0\n255\n"); + fclose(fp); + EXPECT(vpipe_read_pgm(path, &img) < 0); + } snprintf(path, sizeof(path), "%s/huge.pgm", root); fp = fopen(path, "wb"); EXPECT(fp != NULL); - fprintf(fp, "P5\n100000 100000\n255\n"); - fclose(fp); - EXPECT(vpipe_read_pgm(path, &img) < 0); + if (fp) { + fprintf(fp, "P5\n100000 100000\n255\n"); + fclose(fp); + EXPECT(vpipe_read_pgm(path, &img) < 0); + } } static void test_threshold_full(void)