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)