From 3696e30cf29a1678b475ba870fb607d7f6421101 Mon Sep 17 00:00:00 2001 From: Yao-Kuan Tseng Date: Tue, 7 Jul 2026 09:52:53 +0000 Subject: [PATCH] [scripts] introduce static analysis tool - coccicheck --- scripts/coccinelle/api/alloc_cast.cocci | 41 ++++ scripts/coccinelle/api/strdup.cocci | 154 +++++++++++++ scripts/coccinelle/free/ifnullfree.cocci | 40 ++++ scripts/coccinelle/misc/countof.cocci | 63 ++++++ scripts/coccinelle/misc/flexible_array.cocci | 98 ++++++++ scripts/coccinelle/misc/noderef.cocci | 70 ++++++ scripts/coccinelle/misc/semicolon.cocci | 78 +++++++ scripts/do-coccicheck | 226 +++++++++++++++++++ 8 files changed, 770 insertions(+) create mode 100644 scripts/coccinelle/api/alloc_cast.cocci create mode 100644 scripts/coccinelle/api/strdup.cocci create mode 100644 scripts/coccinelle/free/ifnullfree.cocci create mode 100644 scripts/coccinelle/misc/countof.cocci create mode 100644 scripts/coccinelle/misc/flexible_array.cocci create mode 100644 scripts/coccinelle/misc/noderef.cocci create mode 100644 scripts/coccinelle/misc/semicolon.cocci create mode 100755 scripts/do-coccicheck diff --git a/scripts/coccinelle/api/alloc_cast.cocci b/scripts/coccinelle/api/alloc_cast.cocci new file mode 100644 index 000000000..a30134a65 --- /dev/null +++ b/scripts/coccinelle/api/alloc_cast.cocci @@ -0,0 +1,41 @@ +// Coccinelle script to remove redundant casts from memory allocation functions. +// In C, void * is implicitly convertible to any pointer type, so casting the return value of malloc/calloc/realloc/memalign is redundant. + +virtual patch +virtual context +virtual report + +// [Pattern Matcher] Find explicit casts on memory allocation functions +@r1@ +type T; +position p; +@@ + + (T *) + \(malloc@p\|calloc@p\|realloc@p\|memalign@p\)(...) + +// [Filter] Ignore C++ files where void* cast is mandatory +@script:python@ +p << r1.p; +@@ + +if p[0].file.endswith(('.cpp', '.cc', '.cxx', '.hpp', '.hh', '.C', '.H')): + cocci.include_match(False) + +// [Patch Mode] Action: Remove redundant cast from memory allocation functions +@depends on patch@ +type T; +position r1.p; +@@ + +- (T *) + \(malloc@p\|calloc@p\|realloc@p\|memalign@p\)(...) + +// [Report Mode] Output Formatter: Print warning to console +@script:python depends on report@ +p << r1.p; +t << r1.T; +@@ + +msg = "WARNING: casting value returned by memory allocation function to (%s *) is useless in C." % (t) +coccilib.report.print_report(p[0], msg) diff --git a/scripts/coccinelle/api/strdup.cocci b/scripts/coccinelle/api/strdup.cocci new file mode 100644 index 000000000..b58a4682e --- /dev/null +++ b/scripts/coccinelle/api/strdup.cocci @@ -0,0 +1,154 @@ +// Coccinelle script to use strdup() instead of manual memory allocation and string copy. +// strdup(src) is equivalent to malloc(strlen(src) + 1) followed by strcpy/memcpy/strlcpy. + +virtual patch +virtual context +virtual report + +// [Filter] Exclude strdup and strndup implementation functions +@ignore@ +identifier fn = {strdup, strndup}; +position p; +@@ + +fn(...) { + ... +( + malloc@p(...) +| + calloc@p(...) +) + ... +} + +// [Patch Mode] Action: Replace manual allocation and copy (without length variable) with strdup() +@depends on patch@ +expression from, to; +expression E1, E2; +statement S; +position p != ignore.p; +@@ + +( +- to = malloc@p(strlen(from) + 1); ++ to = strdup(from); +| +- to = calloc@p(1, strlen(from) + 1); ++ to = strdup(from); +| +- to = calloc@p(strlen(from) + 1, 1); ++ to = strdup(from); +) + ... when != \(from = E1 \| to = E1 \) + if (to == NULL || ...) S + ... when != \(from = E2 \| to = E2 \) +( +- strcpy(to, from); +| +- strlcpy(to, from, strlen(from) + 1); +| +- memcpy(to, from, strlen(from) + 1); +) + +// [Patch Mode] Action: Replace manual allocation and copy (with length variable) with strdup() +@depends on patch@ +expression x, from, to; +expression E1, E2, E3; +statement S; +position p != ignore.p; +@@ + +- x = strlen(from) + 1; + ... when != \( x = E1 \| from = E1 \) +( +- to = malloc@p(x); ++ to = strdup(from); +| +- to = calloc@p(1, x); ++ to = strdup(from); +| +- to = calloc@p(x, 1); ++ to = strdup(from); +) + ... when != \(x = E2 \| from = E2 \| to = E2 \) + if (to == NULL || ...) S + ... when != \(x = E3 \| from = E3 \| to = E3 \) +( +- memcpy(to, from, x); +| +- strlcpy(to, from, x); +| +- strcpy(to, from); +) + +// [Context/Report Mode] Pattern Matcher: Find manual allocation and copy without length variable +@r1 depends on !patch exists@ +expression from, to; +expression E1, E2; +statement S; +position p1 != ignore.p, p2; +@@ + +( +* to = malloc@p1(strlen(from) + 1); +| +* to = calloc@p1(1, strlen(from) + 1); +| +* to = calloc@p1(strlen(from) + 1, 1); +) + ... when != \(from = E1 \| to = E1 \) + if (to == NULL || ...) S + ... when != \(from = E2 \| to = E2 \) +( +* strcpy@p2(to, from); +| +* strlcpy@p2(to, from, strlen(from) + 1); +| +* memcpy@p2(to, from, strlen(from) + 1); +) + +// [Context/Report Mode] Pattern Matcher: Find manual allocation and copy with length variable +@r2 depends on !patch exists@ +expression x, from, to; +expression E1, E2, E3; +statement S; +position p1 != ignore.p, p2; +@@ + +* x = strlen(from) + 1; + ... when != \( x = E1 \| from = E1 \) +( +* to = malloc@p1(x); +| +* to = calloc@p1(1, x); +| +* to = calloc@p1(x, 1); +) + ... when != \(x = E2 \| from = E2 \| to = E2 \) + if (to == NULL || ...) S + ... when != \(x = E3 \| from = E3 \| to = E3 \) +( +* memcpy@p2(to, from, x); +| +* strlcpy@p2(to, from, x); +| +* strcpy@p2(to, from); +) + +// [Report Mode] Output Formatter: Print warning for r1 to console +@script:python depends on report@ +p1 << r1.p1; +p2 << r1.p2; +@@ + +msg = "WARNING opportunity for strdup (copy on line %s)" % (p2[0].line) +coccilib.report.print_report(p1[0], msg) + +// [Report Mode] Output Formatter: Print warning for r2 to console +@script:python depends on report@ +p1 << r2.p1; +p2 << r2.p2; +@@ + +msg = "WARNING opportunity for strdup (copy on line %s)" % (p2[0].line) +coccilib.report.print_report(p1[0], msg) \ No newline at end of file diff --git a/scripts/coccinelle/free/ifnullfree.cocci b/scripts/coccinelle/free/ifnullfree.cocci new file mode 100644 index 000000000..6f53376d6 --- /dev/null +++ b/scripts/coccinelle/free/ifnullfree.cocci @@ -0,0 +1,40 @@ +// Coccinelle script to remove redundant NULL checks before free(). +// free(NULL) is safe, so "if (x) free(x);" can be simplified to "free(x);". + +virtual patch +virtual context +virtual report + +// [Context/Patch Mode] Pattern Matcher: Find redundant NULL checks +@r depends on context || patch@ +expression x; +position p; +@@ + +* if (x)@p + free(x); + +// [Patch Mode] Action: Remove the redundant 'if' check +@depends on patch && r@ +expression x; +@@ + +- if (x) + free(x); + +// [Report Mode] Pattern Matcher: Find redundant NULL checks +@r2 depends on report@ +expression x; +position p; +@@ + + if (x)@p + free(x); + +// [Report Mode] Output Formatter: Print warning to console +@script:python depends on report@ +p << r2.p; +@@ + +msg = "WARNING: redundant NULL check before free" +coccilib.report.print_report(p[0], msg) diff --git a/scripts/coccinelle/misc/countof.cocci b/scripts/coccinelle/misc/countof.cocci new file mode 100644 index 000000000..7b8042619 --- /dev/null +++ b/scripts/coccinelle/misc/countof.cocci @@ -0,0 +1,63 @@ +// Coccinelle script to use countof macro instead of opencoded division. +// Replace division of sizeof array by sizeof element with countof(E). + +virtual patch +virtual context +virtual report + +// [Filter] Include LK compiler header +@i@ +@@ + +#include + +// [Context Mode] Pattern Matcher: Find division of sizeof array by sizeof element +@depends on context@ +type T; +T[] E; +@@ +( +* (sizeof(E)/sizeof(*E)) +| +* (sizeof(E)/sizeof(E[...])) +| +* (sizeof(E)/sizeof(T)) +) + +// [Patch Mode] Action: Replace opencoded array size division with countof(E) +@depends on patch@ +type T; +T[] E; +@@ +( +- (sizeof(E)/sizeof(*E)) ++ countof(E) +| +- (sizeof(E)/sizeof(E[...])) ++ countof(E) +| +- (sizeof(E)/sizeof(T)) ++ countof(E) +) + +// [Report Mode] Pattern Matcher: Find division of sizeof array by sizeof element +@r depends on report@ +type T; +T[] E; +position p; +@@ +( + (sizeof(E)@p /sizeof(*E)) +| + (sizeof(E)@p /sizeof(E[...])) +| + (sizeof(E)@p /sizeof(T)) +) + +// [Report Mode] Output Formatter: Print warning for r to console +@script:python depends on report@ +p << r.p; +@@ + +msg = "WARNING: Use countof" +coccilib.report.print_report(p[0], msg) diff --git a/scripts/coccinelle/misc/flexible_array.cocci b/scripts/coccinelle/misc/flexible_array.cocci new file mode 100644 index 000000000..9b0ad5a80 --- /dev/null +++ b/scripts/coccinelle/misc/flexible_array.cocci @@ -0,0 +1,98 @@ +// Coccinelle script to detect zero-length and one-element arrays at the end of structures. +// Zero-length and one-element arrays are deprecated; standard C99 flexible-array members should be used instead. + +virtual context +virtual report +virtual patch + +// [Context/Report Mode] Pattern Matcher: Find zero-length and one-element arrays at structure ends +@r depends on !patch@ +identifier name, array; +type T; +position p; +@@ + +( + struct name { + ... +* T array@p[\(0\|1\)]; + }; +| + struct { + ... +* T array@p[\(0\|1\)]; + }; +| + union name { + ... +* T array@p[\(0\|1\)]; + }; +| + union { + ... +* T array@p[\(0\|1\)]; + }; +) + +// [Filter] Exclude structures where array is the only member +@only_field depends on patch@ +identifier name, array; +type T; +position q; +@@ + +( + struct name {@q + T array[\(0\|1\)]; + }; +| + struct {@q + T array[\(0\|1\)]; + }; +) + +// [Patch Mode] Action: Remove zero-length or one-element specification in flexible-array member +@depends on patch@ +identifier name, array; +type T; +position p; +position q != only_field.q; +@@ + +( + struct name {@q + ... + T array@p[ +- 0 + ]; + }; +| + struct name {@q + ... + T array@p[ +- 1 + ]; + }; +| + struct {@q + ... + T array@p[ +- 0 + ]; + }; +| + struct {@q + ... + T array@p[ +- 1 + ]; + }; +) + +// [Report Mode] Output Formatter: Print warning for r to console +@script:python depends on report@ +p << r.p; +@@ + +msg = "WARNING: use C99 flexible-array member instead of zero-length or one-element array" +coccilib.report.print_report(p[0], msg) diff --git a/scripts/coccinelle/misc/noderef.cocci b/scripts/coccinelle/misc/noderef.cocci new file mode 100644 index 000000000..7eaf5aca6 --- /dev/null +++ b/scripts/coccinelle/misc/noderef.cocci @@ -0,0 +1,70 @@ +// Coccinelle script to detect sizeof applied to pointer expressions. +// Applying sizeof to a pointer expression x gives the size of the pointer, not the dereferenced size *x. + +virtual report +virtual context +virtual patch + +// [Patch Mode] Action: Replace sizeof(x) with sizeof(*x) for pointer expressions +@depends on patch@ +expression *x; +expression f; +expression i; +type T; +@@ + +( +x = <+... sizeof( +- x ++ *x + ) ...+> +| +f(...,(T)(x),...,sizeof( +- x ++ *x + ),...) +| +f(...,sizeof( +- x ++ *x + ),...,(T)(x),...) +| +f(...,(T)(x),...,i*sizeof( +- x ++ *x + ),...) +| +f(...,i*sizeof( +- x ++ *x + ),...,(T)(x),...) +) + +// [Context/Report Mode] Pattern Matcher: Find sizeof applied to pointer expressions +@r depends on !patch@ +expression *x; +expression f; +expression i; +position p; +type T; +@@ + +( +*x = <+... sizeof@p(x) ...+> +| +*f(...,(T)(x),...,sizeof@p(x),...) +| +*f(...,sizeof@p(x),...,(T)(x),...) +| +*f(...,(T)(x),...,i*sizeof@p(x),...) +| +*f(...,i*sizeof@p(x),...,(T)(x),...) +) + +// [Report Mode] Output Formatter: Print error for r to console +@script:python depends on report@ +p << r.p; +@@ + +msg = "ERROR: application of sizeof to pointer" +coccilib.report.print_report(p[0], msg) diff --git a/scripts/coccinelle/misc/semicolon.cocci b/scripts/coccinelle/misc/semicolon.cocci new file mode 100644 index 000000000..1e5db0bf3 --- /dev/null +++ b/scripts/coccinelle/misc/semicolon.cocci @@ -0,0 +1,78 @@ +// Coccinelle script to remove unneeded double semicolons. +// Accidental extra semicolons after statements or switch cases are redundant and should be removed. + +virtual patch +virtual report +virtual context + +// [Pattern Matcher] Find switch default case semicolons +@r_default@ +position p; +@@ +switch (...) +{ +default: ...;@p +} + +// [Pattern Matcher] Find switch case semicolons +@r_case@ +position p; +@@ +( +switch (...) +{ +case ...:;@p +} +| +switch (...) +{ +case ...:... +case ...:;@p +} +| +switch (...) +{ +case ...:... +case ...: +case ...:;@p +} +) + +// [Pattern Matcher] Find unneeded extra semicolons after statements +@r1@ +statement S; +position p1; +position p != {r_default.p, r_case.p}; +identifier label; +@@ +( +label:; +| +S@p1;@p +) + +// [Filter] Ignore semicolons on different lines from statement end +@script:python@ +p << r1.p; +p1 << r1.p1; +@@ +if p[0].line != p1[0].line_end: + cocci.include_match(False) + +// [Patch Mode] Action: Remove unneeded extra semicolon +@depends on patch@ +position r1.p; +@@ +-;@p + +// [Report Mode] Output Formatter: Print warning for r1 to console +@script:python depends on report@ +p << r1.p; +@@ +coccilib.report.print_report(p[0], "WARNING: Unneeded semicolon") + +// [Context Mode] Pattern Matcher: Highlight unneeded extra semicolon +@depends on context@ +position r1.p; +@@ +*;@p diff --git a/scripts/do-coccicheck b/scripts/do-coccicheck new file mode 100755 index 000000000..99e6da264 --- /dev/null +++ b/scripts/do-coccicheck @@ -0,0 +1,226 @@ +#!/usr/bin/env bash + +# Section 1: Verify that the project argument is provided +if [ "$#" -lt 1 ]; then + echo "usage: $0 [COCCI=...] [M=...] [MODE=...] [J=jobs] [V=1] [S=severity]" + exit 1 +fi + +PROJ=$1 +shift + +# Default values +COCCI="" +M="" +MODE="report" +VERBOSE=0 +SEVERITY="" +JOBS=$(nproc 2>/dev/null || getconf _NPROCESSORS_ONLN 2>/dev/null || echo 1) + +# Section 2: Parse optional configuration arguments (COCCI, M, MODE, V, S) +while [ "$#" -gt 0 ]; do + case "$1" in + COCCI=*) + COCCI="${1#COCCI=}" + ;; + M=*) + M="${1#M=}" + ;; + MODE=*) + MODE="${1#MODE=}" + ;; + V=*|VERBOSE=*) + VERBOSE="${1#*=}" + ;; + S=*|SEVERITY=*) + SEVERITY="${1#*=}" + ;; + J=*|JOBS=*) + JOBS="${1#*=}" + ;; + *) + echo "Unknown argument: $1" + echo "usage: $0 [COCCI=...] [M=...] [MODE=...] [J=jobs] [V=1] [S=severity]" + exit 1 + ;; + esac + shift +done + +PROJ_DIR=build-${PROJ} + +# Section 3: Check if the project build directory and metadata files exist +if [ ! -d "$PROJ_DIR" ]; then + echo "Project build directory $PROJ_DIR does not exist. Please run make first." + exit 1 +fi + +if [ ! -f "${PROJ_DIR}/srcfiles.txt" ] || [ ! -f "${PROJ_DIR}/include_paths.txt" ]; then + echo "Metadata files missing. Please ensure project is built." + exit 1 +fi + +# Section 4: Verify that spatch (Coccinelle) is installed +if ! command -v spatch >/dev/null 2>&1; then + echo "Error: 'spatch' (Coccinelle) is not installed or not found in PATH." >&2 + echo "Please install Coccinelle (e.g., 'sudo apt install coccinelle' or from source)." >&2 + exit 1 +fi + +# Section 5: Resolve COCCI parameter to a list of target .cocci files +COCCI_DIR="scripts/coccinelle" +COCCI_FILES="" + +if [ -z "$COCCI" ]; then + COCCI_FILES=$(find "$COCCI_DIR" -name "*.cocci" -type f | sort) +else + if [ -d "$COCCI_DIR/$COCCI" ]; then + COCCI_FILES=$(find "$COCCI_DIR/$COCCI" -name "*.cocci" -type f | sort) + elif [ -d "$COCCI" ]; then + COCCI_FILES=$(find "$COCCI" -name "*.cocci" -type f | sort) + elif [ -f "$COCCI" ]; then + COCCI_FILES="$COCCI" + elif [ -f "$COCCI_DIR/$COCCI" ]; then + COCCI_FILES="$COCCI_DIR/$COCCI" + elif [ -f "$COCCI_DIR/$COCCI.cocci" ]; then + COCCI_FILES="$COCCI_DIR/$COCCI.cocci" + else + # Shortcut search (no slash) + case "$COCCI" in + */*) + echo "Coccinelle script/dir '$COCCI' not found" + exit 1 + ;; + *) + SEARCH_RESULT=$(find "$COCCI_DIR" \( -name "$COCCI" -o -name "$COCCI.cocci" \) -type f) + if [ -z "$SEARCH_RESULT" ]; then + echo "Coccinelle script '$COCCI' not found in $COCCI_DIR" + exit 1 + fi + NUM_FOUND=$(echo "$SEARCH_RESULT" | wc -l) + if [ "$NUM_FOUND" -eq 1 ]; then + COCCI_FILES="$SEARCH_RESULT" + else + echo "Multiple cocci files found for '$COCCI':" + echo "$SEARCH_RESULT" + COCCI_FILES=$(echo "$SEARCH_RESULT" | head -n 1) + echo "Picking first: $COCCI_FILES" + fi + ;; + esac + fi +fi + +if [ -z "$COCCI_FILES" ]; then + echo "No Coccinelle scripts found to run." + exit 1 +fi + +# Section 6: Convert include paths from build metadata into an array of -I flags for spatch +INCLUDES=() +while IFS= read -r line; do + [ -z "$line" ] && continue + INCLUDES+=(-I "$line") +done < "${PROJ_DIR}/include_paths.txt" + +# Section 7: Filter C source files from build metadata into an array and apply directory filter (M) +C_SRCS=() +M_NO_SLASH="${M%/}" +while IFS= read -r line; do + case "$line" in + *.c) + if [ -f "$line" ]; then + if [ -n "$M" ]; then + case "$line" in + "$M_NO_SLASH"/*|"$M_NO_SLASH") + C_SRCS+=("$line") + ;; + esac + else + C_SRCS+=("$line") + fi + fi + ;; + esac +done < "${PROJ_DIR}/srcfiles.txt" + +if [ ${#C_SRCS[@]} -eq 0 ]; then + echo "No C source files found to check." + exit 0 +fi + +# Section 8: Set up global spatch execution flags (quiet, includes, diff suppression) +SPATCH_FLAGS="" +if [ -f "${PROJ_DIR}/config.h" ]; then + SPATCH_FLAGS="--include ${PROJ_DIR}/config.h" +fi + +SPATCH_FLAGS="$SPATCH_FLAGS --all-includes" + +# Set parallel jobs (default to all CPUs) +if [ -n "$JOBS" ] && [ "$JOBS" -gt 1 ] 2>/dev/null; then + SPATCH_FLAGS="$SPATCH_FLAGS --jobs $JOBS" +fi + +# Add quiet flags if not verbose +if [ "$VERBOSE" -ne 1 ]; then + SPATCH_FLAGS="$SPATCH_FLAGS --very-quiet" +fi + +# Add no-show-diff for report/org modes +if [ "$MODE" = "report" ] || [ "$MODE" = "org" ]; then + SPATCH_FLAGS="$SPATCH_FLAGS --no-show-diff" +fi + +filter_output() { + if [ -n "$SEVERITY" ]; then + grep -F -i -- "$SEVERITY" || true + else + cat + fi +} + +# Section 9: Print execution banner summarizing the run configuration +if [ "$VERBOSE" -ne 1 ]; then + NUM_FILES=$(echo "$COCCI_FILES" | wc -l) + echo "==================================================" + echo " Running Coccinelle (coccicheck)" + echo " Target Project : $PROJ" + echo " Target Directory : ${M:-Whole Project}" + echo " Cocci Script : ${COCCI:-All} ($NUM_FILES files)" + echo " Analysis Mode : $MODE" + echo " Concurrency : $JOBS jobs" + echo " Severity Filter : ${SEVERITY:-All}" + echo "==================================================" +fi + +# Section 10: Loop through and execute each Coccinelle script, formatting the output +set -o pipefail +for f in $COCCI_FILES; do + display_f="${f#$COCCI_DIR/}" + + FILE_SPATCH_FLAGS="$SPATCH_FLAGS" + if grep -q "^virtual" "$f"; then + FILE_SPATCH_FLAGS="$FILE_SPATCH_FLAGS -D $MODE" + fi + + if [ "$VERBOSE" -eq 1 ]; then + echo "spatch --sp-file $f $FILE_SPATCH_FLAGS ${INCLUDES[*]} [${#C_SRCS[@]} C sources...]" + spatch --sp-file "$f" $FILE_SPATCH_FLAGS "${INCLUDES[@]}" "${C_SRCS[@]}" || exit 1 + else + OUTPUT=$(spatch --sp-file "$f" $FILE_SPATCH_FLAGS "${INCLUDES[@]}" "${C_SRCS[@]}" 2>&1 | filter_output | sed "s|^|[$display_f] |") + STATUS=$? + if [ -n "$OUTPUT" ]; then + echo "$OUTPUT" + fi + if [ $STATUS -ne 0 ]; then + exit $STATUS + fi + fi +done + +if [ "$VERBOSE" -ne 1 ]; then + echo "==================================================" + echo " Coccinelle analysis complete." + echo "==================================================" +fi