Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .ci/check-cppcheck.sh
Original file line number Diff line number Diff line change
@@ -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[@]}"
28 changes: 28 additions & 0 deletions .ci/check-format.sh
Original file line number Diff line number Diff line change
@@ -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
20 changes: 20 additions & 0 deletions .ci/check-newline.sh
Original file line number Diff line number Diff line change
@@ -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
43 changes: 43 additions & 0 deletions .ci/check-security.sh
Original file line number Diff line number Diff line change
@@ -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
107 changes: 107 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -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"
6 changes: 5 additions & 1 deletion user/vpipe-dump-pgm.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
16 changes: 10 additions & 6 deletions user/vpipe-unit-test.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down